kurumera
Drive a Kurumera store from Python — the platform's tools, the platform's permissions.
pip install kurumera
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, and the browser does not have to be on the same machine:
python -m kurumera login # shows a short code; approve it in a browser
python -m kurumera status # what is signed in, without showing the token
Open https://kurumera.com/oauth/device
Code WXYZ-2468
from kurumera import Kurumera
km = Kurumera.login() # the same, from Python
km = Kurumera() # afterwards, the saved token is found automatically
This is the OAuth 2.1 device authorization grant (RFC 8628), the same exchange an MCP client performs, discovered from the MCP endpoint itself. Nothing is pasted back: the process polls for its own token while you approve. It works identically on a laptop, in a container, over SSH and in CI. Tokens refresh silently before they expire.
Against a server too old to offer that grant, login() falls back to a
browser-redirect flow that needs the browser on this machine, and
login --loopback --manual falls back again to pasting the redirect URL.
A provisioned agent, with a key. This is what a sandbox with no browser uses:
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:
Kurumera(api_key="tps_…")KURUMERA_API_KEYin the environment~/.kurumera/config.json— written bykurumera login
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:
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.
km = Kurumera(agent="builder") # page-builder tools, read-only commerce
Safety you can switch on
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
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:
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:
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:
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
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
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:
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.
Release files for kurumera 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kurumera-0.5.0.tar.gz | 270.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kurumera-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 536.2 kB
Release files / kurumera-0.5.0.tar.gz
| Download URL | kurumera-0.5.0.tar.gz |
|---|---|
| Size | 270.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
347e2c9cc114065d9d18644b2413e253fe71db7962f984b92a27250b07e12852
|
|
BLAKE2b-256 checksum How to use checksums |
bc8797afabf3f87173910096febefe7b0bf63defc91cbd3044b10296880131fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Release files / kurumera-0.5.0-py3-none-any.whl
| Download URL | kurumera-0.5.0-py3-none-any.whl |
|---|---|
| Size | 265.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ee0c65401f24c1b5e4968b6c8301327502f2a3f7d4c88fe0a30338ad1f366524
|
|
BLAKE2b-256 checksum How to use checksums |
32408def836a2f745179d6fe68caec512eb3658e94aa25bd069b03df0a3b2ec5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|