Metadata-Version: 2.4
Name: xoople-sdk
Version: 0.6.0
Summary: Official Python SDK for the Xoople Products API.
License-Expression: LicenseRef-Xoople-Proprietary
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3.12
Requires-Dist: requests>=2.28.1,<3
Requires-Dist: pydantic>=2.8.2
Requires-Dist: truststore>=0.10
Requires-Dist: delta-sharing>=1.3.2 ; extra == 'sharing'
Requires-Dist: pandas>=2.2 ; extra == 'sharing'
Requires-Python: >=3.12
Project-URL: Homepage, https://xoople.com
Provides-Extra: sharing
Description-Content-Type: text/markdown

# xoople-sdk

Official Python SDK for the Xoople Products API.

```python
from xoople.sdk import Config, XoopleClient, StaticToken, models

client = XoopleClient.from_credentials(
    "https://api.xoople.com", StaticToken("...")
)
# Or pass the complete transport configuration directly:
# client = XoopleClient(Config.from_env())

estimate = client.analyses.estimate(request)
analysis = client.analyses.create(request)

# `list` returns a paginator: iterate it for items across every page.
for run in client.analyses.runs.list(analysis.uid):
    print(run.state)

# Sub-resources take the parent analysis uid:
output = client.analyses.outputs.get(analysis.uid, output_uid)
access = client.analyses.outputs.generate_access_url(analysis.uid, output_uid, ttl_seconds=900)

# A table output resolves to a handle that reads itself:
table = client.table(analysis.uid)          # or client.table(uid, output=output_uid)
print(table.shared)                         # measures, grid, size, created time
df = table.to_pandas()                      # pip install "xoople-sdk[sharing]"
sdf = table.to_spark(spark)                 # your session, your connector

# Select an inclusive Delta Change Data Feed range. Start at 0 on the first sync.
changes = table.changes(starting_version=0)
change_df = changes.to_pandas()
change_sdf = changes.to_spark(spark)
latest = table.latest_changes()
latest_df = latest.to_pandas()
latest_sdf = latest.to_spark(spark)

# Custom verbs are plain methods:
client.analyses.cancel(analysis.uid)
client.analyses.schedules.pause(analysis.uid, schedule_uid)

# Cross-analysis reads and catalogs are top-level:
client.schedules.list(state=models.ScheduleStatus.ACTIVE)
client.usage.limits()
client.catalog.measures(product="tabular_time_series")
```

`Config.from_env()` reads `XOOPLE_API_URL` and `XOOPLE_TOKEN`.

## API keys

An API key lets a script or a scheduled job call the API without a browser
sign-in. Create one on the **API keys** section of your Xoople profile page
(`/profile`). The key starts with `sk_` and is shown once, so store it in a
secret manager.

A key belongs to one organization membership. It acts as you, in the
organization that was active when you created it, with that membership's roles.
To work in a different organization, create a key there.

The SDK sends the key as the bearer token. Set `XOOPLE_TOKEN` to the key and
use `Config.from_env()`, or pass it explicitly:

```python
import os

from xoople.sdk import StaticToken, XoopleClient

client = XoopleClient.from_credentials(
    "https://api.xoople.com", StaticToken(os.environ["XOOPLE_TOKEN"])
)
```

The API accepts at most 300 API key requests a minute from one IP address. The
SDK retries rate-limited GETs and replay-safe writes with backoff. Any
rate-limited request that is not retried, or still fails after the retries,
raises `xoople.sdk.errors.RateLimitError`. Revoke a key on the same profile page
when you no longer need it.

## Connection profiles

`Config.from_profile(path)` reads the URL and the credential from one file, for tools
that take a file and not an environment:

```json
{
  "version": 1,
  "api_url": "https://gateway.xoople.com",
  "auth": { "type": "bearer", "token": "<token>" },
  "expires_at": "2026-09-30T00:00:00Z"
}
```

To use the sign-in from `xoople auth login` instead of a token, set
`"auth": { "type": "xoople-cli" }`. The profile then names only the API, and the
session is read from `$XDG_CONFIG_HOME/xoople/session.json` (else
`~/.config/xoople/session.json`) and refreshed in place. `xoople.sdk.session` reads and
writes that file. The profile must name the API the session signed in for, so sign in
with `xoople auth login --api-url <url>`; another API raises `CredentialError`.

`expires_at` is optional and is what the issuer stamped. It carries a UTC offset. A
profile past it raises `ProfileExpiredError` on load, not at the first request.

The file is a credential. Keep it out of projects and repositories. Restrict who can
read it.

`client.table()` is the authenticated half of `xoople.sdk.sharing`, which reads
a `.share` profile with no API credential at all. It downloads one profile for
the client's lifetime and removes it on `close()`, so keep the client open
through every read — Spark reads lazily. `client.table(uid,
profile_path=...)` uses a profile you own instead; the SDK never writes or
removes that one.

Change Data Feed reads accept either `starting_version` with an optional
`ending_version`, or `starting_timestamp` with an optional `ending_timestamp`.
Ranges are inclusive and return the standard `_change_type`, `_commit_version`,
and `_commit_timestamp` columns. Persist the highest commit version you apply,
then begin the next read at that value plus one. For inspection rather than
cursor-based synchronization, `latest_changes().to_pandas()` and
`latest_changes().to_spark(spark)` read only the newest available table commit.

Every failure raises a subclass of `xoople.sdk.errors.XoopleError`.
