Metadata-Version: 2.5
Name: kurumera
Version: 0.4.0
Summary: Drive a Kurumera store from Python - the platform's tools, the platform's permissions.
Project-URL: Homepage, https://kurumera.com
Project-URL: Documentation, https://kurumera.com
Author: Kurumera
License-Expression: MIT
License-File: LICENSE
Keywords: agent,ecommerce,kurumera,mcp,storefront
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# kurumera

Drive a Kurumera store from Python — the platform's tools, the platform's permissions.

```bash
pip install kurumera
```

```python
from kurumera import Kurumera

km = Kurumera()                                  # key from KURUMERA_API_KEY

km.create_product(title="Badam 500g", product_type="Dry Fruit")
for product in km.paginate(km.list_products, limit=100):
    print(product["title"])
```

Every method is one of the platform's tools, under the same name it has everywhere else.
Anything you already know about `create_product` or `apply_page_edits` is still true here.

---

## Signing in

Two ways in, the same two the MCP interface has.

A person, with a browser. No key to create, no key to paste:

```bash
python -m kurumera login      # opens a browser, approve once
python -m kurumera status     # what is signed in, without showing the token
```

**If the browser is on a different machine** — a cloud container, a remote
shell, CI — the loopback redirect cannot reach you and no retry will change
that. Two commands instead:

```bash
python -m kurumera login --manual              # prints a link, exits
# approve it; the browser fails to load 127.0.0.1; copy that url
python -m kurumera login --code "<that url>"
```

A pasted redirect url is not a credential. The code in it is single use,
expires in minutes, and is worthless without the PKCE verifier, which never
leaves the machine that started the sign-in.

```python
from kurumera import Kurumera
km = Kurumera.login()         # the same, from Python
km = Kurumera()               # afterwards, the saved token is found automatically
```

It is the same OAuth 2.1 exchange an MCP client performs, against the same
endpoints, discovered from the MCP endpoint itself. PKCE is mandatory, no client
secret exists, and the redirect lands on loopback so the code is handed straight
back to the process rather than copied by hand. Tokens are refreshed silently
before they expire.

A provisioned agent, with a key. This is what a sandbox with no browser uses:

```python
km = Kurumera(api_key="tps_...")     # or KURUMERA_API_KEY in the environment
```

A key given deliberately always outranks a saved sign-in.

## The store comes from your key

There is no tenant, store or shop argument. Not in the constructor, not per call, not as an
environment variable. The server works out which store you mean from your credential and
ignores anything a client claims.

That is a security property, not an omission: a client that could name a store is a client a
prompt-injected agent could aim at someone else's data by inventing an id. This package
refuses tenant-shaped arguments even on the dynamic `call()` path.

## Credentials

First one found wins:

1. `Kurumera(api_key="tps_…")`
2. `KURUMERA_API_KEY` in the environment
3. `~/.kurumera/config.json` — written by `kurumera login`

```bash
export KURUMERA_API_KEY=tps_…
export KURUMERA_AGENT=store        # optional: 'store' or 'builder'
```

**In a sandbox with an unwritable `$HOME`**, point the config elsewhere with
`KURUMERA_CONFIG_DIR=/tmp/kurumera`. The SDK only ever reads that file.

Mint a key with the scopes an agent should have:

```bash
python manage.py create_api_key --user you@example.com --name "sdk" \
  --scopes read_products write_products read_orders …
```

## Personas

`agent="store"` or `agent="builder"` narrows the tool set to exactly what the platform's
provisioned assistants get. It is a **cap, never a grant** — it can only take tools away
from a key, never add them.

```python
km = Kurumera(agent="builder")     # page-builder tools, read-only commerce
```

## Safety you can switch on

```python
km = Kurumera(read_only=True)             # refuses every writing tool, locally
km = Kurumera(allow_destructive=False)    # refuses deletes, publishes, overwrites
km = Kurumera(on_destructive=ask_a_human) # (tool, args, info) -> bool
```

`read_only=True` is the highest-value line in this file if your agent reads anything it did
not write. It makes an over-scoped key harmless for the duration of a reading task, and it
refuses before the request leaves the process. It **fails closed**: a tool this package has
never heard of is refused too.

These are advisory. The server enforces its own rules regardless — they exist so a
misdirected agent is stopped early and told which policy stopped it.

## Saying why

```python
with km.intent("Restocking after the spring sale"):
    km.bulk_update_products(product_ids=ids, action="set_status", value="ACTIVE")
```

The merchant sees that sentence beside the call in their activity feed. It is the difference
between *"something changed 40 products"* and an explanation.

## Finding a tool

264 is too many to remember and `dir()` gives a flat wall of names. Discovery is
progressive, offline, and costs no round trip:

```python
print(km.help())                   # the subjects
print(km.help("low stock"))        # a plain-English phrase works
print(km.help("adjust_inventory")) # the full signature, types and warnings
```

```
adjust_inventory(inventory_item_id: str, location_id: str, delta: int)
  [write]  module: inventory_tools
```

`km.catalog()` groups every tool by subject, `km.search()` returns the same
ranking as objects, and `km.has("name")` says whether a typed method exists.

Use `km.has()` rather than `hasattr`, which is always True: an unknown attribute
becomes a dynamic call so a tool newer than this package still works.

## Results

A result behaves as the payload itself, because that is what every existing doc assumes:

```python
r = km.list_products(limit=5)
r["products"]          # the rows, under the tool's OWN noun
r["total"]             # how many exist in all
r.data                 # the same mapping
r.text                 # the first text block, verbatim
r.raw                  # the untouched envelope
```

There is no shared `results` envelope: `list_products` answers under `products`,
`list_content` under `pages`, `list_collections` under `collections`. Use `km.paginate`
rather than looping yourself and the difference stops mattering.

Ten tools can also return an image — screenshots and product photos:

```python
shot = km.show_page_screenshot(page_ref="home", full_page=True)
shot.data["page_id"]
if shot.images:
    shot.images[0].save("home.png")
```

`shot.images` is empty when capture was skipped. That is a normal outcome, not an error —
a screenshot must never fail a write that already succeeded.

## Errors

```python
from kurumera import RateLimited, PermissionDenied, ConfirmationRequired

try:
    km.delete_product(product_id=pid)
except ConfirmationRequired:
    km.delete_product(product_id=pid, confirm=True)
except PermissionDenied as e:
    print("this key lacks", e.capability)
except RateLimited as e:
    print("slow down", e.retry_after, "seconds")
```

| Exception | When |
|---|---|
| `KurumeraConfigError` | no credential, bad URL, a tenant argument — nothing was sent |
| `AuthRequired` / `AuthFailed` | 401 |
| `SubscriptionInactive` | 402 — the store's subscription will not serve API traffic |
| `TenantInactive`, `NoTenant`, … | 403 |
| `PermissionDenied` | the key or role lacks the tool's capability; `.capability` names it |
| `ConfirmationRequired` | pass `confirm=True` |
| `InvalidArguments` | the arguments failed the tool's schema, server-side |
| `RateLimited` | 240 reads / 60 writes a minute; `.retry_after` when the server said |
| `ToolNotFound` | no such tool for this key |
| `ReadOnlyModeError`, `DestructiveBlockedError` | this client's own policy refused |

**Rate limits are not retried for you.** The window is a fixed minute and an agent sandbox
gives a script two; sleeping blind would spend most of your budget hiding a signal you
should act on. Pass `retry_on_rate_limit=True` if you are running a batch and mean it.

## Finding tools

```python
km.tools()                    # what THIS key may call — asks the server
km.describe("get_report")     # schema, hints, whether it needs confirm
km.search("invoice")          # offline, over names and descriptions
km.check()                    # is this package in step with the server?
```

`km.tools()` is the honest answer: the server has already filtered it by your key's
capabilities and persona. The offline list (`source="package"`) says nothing about what
*you* are allowed to do.

## Timeouts

The gateway in front of the platform closes a read at **60 seconds**, so the default client
timeout is 65 — just above it, so you get the server's honest error rather than a confusing
client-side abort. Raising it accomplishes nothing. Uploads are capped around 18 MB of
actual file once base64 inflation is counted.

## Versions

`kurumera.REGISTRY_TOOL_COUNT` and `kurumera.REGISTRY_HASH` say which tool registry this
build was generated from. A tool added to the platform since then is still callable:

```python
km.call("a_tool_added_last_week", some_argument=1)
```

An SDK version lag is a typing gap, not an outage.

## Releasing

The release procedure, the pre-flight gates and the three things about PyPI that cannot
be undone are in [PUBLISHING.md](PUBLISHING.md).
