Metadata-Version: 2.4
Name: tokengate
Version: 0.1.0
Summary: Python SDK for TokenGate — quota management as a service
Project-URL: Homepage, https://tokengate.rodmena.co.uk
Project-URL: Documentation, https://tokengate.rodmena.co.uk/llms.txt
Author-email: Rodmena Limited <info@rodmena.co.uk>
License: Apache-2.0
License-File: LICENSE
Keywords: api,billing,llm,metering,quota,rate-limiting,usage
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.24
Requires-Dist: pydantic>=2.5
Description-Content-Type: text/markdown

# tokengate

Python SDK for TokenGate — quota management as a service.

```bash
pip install tokengate
```

Requires Python 3.11+. Sync and async clients, automatic idempotency keys, a
reserve/commit metering context manager, and typed exceptions for every
denial. The full integration contract lives at your deployment's `/llms.txt`
(and is served over MCP at `/mcp/`).

```python
from tokengate import TokenGate

client = TokenGate("https://tokengate.example.com", api_key="tg_...")

# Atomic check-and-consume (idempotency key auto-generated)
result = client.consume("user_42", {"llm.input_tokens": 1200, "llm.output_tokens": 350})
print(result.allowed, [(r.resource, r.remaining) for r in result.results])

# LLM-style metering: reserve an estimate, commit actual usage
with client.meter("user_42", {"llm.tokens": 4000}) as m:
    completion = run_llm(...)
    m.record("llm.tokens", completion.usage.total_tokens)
# exit: commit(actuals) on success, release() on exception

# Denials raise typed errors carrying retry hints
from tokengate import QuotaExceeded, RateLimited

try:
    client.consume("user_42", {"llm.tokens": 10_000_000})
except QuotaExceeded as exc:
    print(exc.retry_after, exc.reset_at, exc.blocking_policy)
```

`AsyncTokenGate` mirrors the same surface with `async`/`await` and
`async with client.meter(...)`.

**Denials are exceptions, never return values.** `consume()` refusals arrive
as RFC 7807 429s and raise `QuotaExceeded`/`RateLimited`; a success status
therefore always means permission. The client enforces that invariant rather
than assuming it: if a 2xx body ever carried `allowed: false`, `consume()`
raises `ProtocolViolation` instead of returning — a denial must never be
readable as a grant. The advisory `check()` is the exception by design: it
always answers 200 and you inspect `response.allowed` yourself.
