Metadata-Version: 2.4
Name: apikeys-platform
Version: 0.1.2
Summary: Async SDK for issuing and validating scoped API keys
Author: AKSHILMY
License-Expression: MIT
Project-URL: Repository, https://github.com/AKSHILMY/api-project
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Framework :: AsyncIO
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: pydantic<3,>=2.0
Requires-Dist: sqlalchemy[asyncio]<3,>=2.0
Requires-Dist: alembic>=1.13
Requires-Dist: greenlet>=3.0
Provides-Extra: sqlite
Requires-Dist: aiosqlite>=0.19; extra == "sqlite"
Provides-Extra: postgresql
Requires-Dist: asyncpg>=0.29; extra == "postgresql"
Requires-Dist: psycopg2-binary>=2.9; extra == "postgresql"
Provides-Extra: mysql
Requires-Dist: aiomysql>=0.2; extra == "mysql"
Provides-Extra: all
Requires-Dist: apikeys-platform[mysql,postgresql,sqlite]; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: aiosqlite>=0.19; extra == "dev"

# apikeys-platform

Async Python SDK for issuing, scoping, and validating API keys — backed by SQLite or PostgreSQL.

Designed for developers who want to add API key management to their own product without building the infrastructure from scratch.

## Installation

```bash
# SQLite (development / small deployments)
pip install "apikeys-platform[sqlite]"

# PostgreSQL (production)
pip install "apikeys-platform[postgresql]"

# MySQL (production)
pip install "apikeys-platform[mysql]"

# All drivers
pip install "apikeys-platform[all]"
```

Requires Python 3.11+.

## Quickstart

`create_tables` and `APIKeyClient` both accept plain URLs — no need to specify the async driver suffix:

```python
import asyncio
from apikeys import APIKeyClient, KeyMetadata, create_tables

async def main():
    # SQLite for dev:   "sqlite:///myapp.db"
    # PostgreSQL prod:  "postgresql://user:pass@host:5432/dbname"
    # MySQL prod:       "mysql://user:pass@host:3306/dbname"
    db_url = "sqlite:///myapp.db"
    await create_tables(db_url)   # creates tables on first run; safe to call on every restart
    client = APIKeyClient(db_url)

    # One-time setup
    org     = await client.create_organization("Acme Inc")
    product = await client.create_product(str(org.id), "My API")
    project = await client.create_project(str(org.id), "v1")
    await client.add_product_to_project(str(product.id), str(project.id))

    # Issue a key for one of your users
    result = await client.create_key(
        str(org.id),
        project_id=str(project.id),
        product_id=str(product.id),
        metadata=KeyMetadata(
            name="Alice's key",
            scopes=["read", "write"],
            rate_limit=1000,
            custom={"user_id": "u_alice", "plan": "pro"},
        ),
    )
    print(result.plaintext)   # return this once to your user

    # Validate an incoming request
    key = await client.validate_key(
        result.plaintext,
        product_id=str(product.id),
        required_scope="read",
    )
    print(key.metadata.custom["user_id"])   # → u_alice

asyncio.run(main())
```

## Key concepts

| Concept | Description |
|---------|-------------|
| **Organization** | Top-level tenant — maps to one of your customers or your own company |
| **Project** | Groups keys within an org (e.g. `v1`, `v2`, `mobile`) |
| **Product** | A named API surface linked to projects; keys can be scoped to one product |
| **`KeyMetadata.custom`** | Arbitrary JSON attached to a key — store `user_id`, `plan`, `tenant_id`, etc. |

Keys are scoped from broad → narrow: **org-wide → project-scoped → product-scoped**.  
`validate_key()` checks the key is active, matches the expected product, and holds the required scope.

## Exceptions

```python
from apikeys import InvalidKeyError, RevokedKeyError, InsufficientScopeError
```

| Exception | When raised |
|-----------|-------------|
| `InvalidKeyError` | Key not found |
| `RevokedKeyError` | Key exists but has been revoked |
| `InsufficientScopeError` | Key is valid but missing the required scope |

## Full example

End-to-end script showing one-time setup, per-user key creation, request validation, and listing keys by user:

```python
import asyncio
from apikeys import APIKeyClient, KeyMetadata
from apikeys.db.session import create_tables

async def main() -> None:
    DB_URL = "sqlite+aiosqlite:///acme_demo.db"
    await create_tables(DB_URL)
    client = APIKeyClient(DB_URL)

    # ── One-time setup ───────────────────────────────────────────────────────
    org     = await client.create_organization("Acme Corp")
    product = await client.create_product(str(org.id), "Acme App")
    project = await client.create_project(str(org.id), "Acme API v1")
    await client.add_product_to_project(str(product.id), str(project.id))

    ORG_ID     = str(org.id)
    PRODUCT_ID = str(product.id)
    PROJECT_ID = str(project.id)

    # ── Issue a key for a user ───────────────────────────────────────────────
    result = await client.create_key(
        ORG_ID,
        project_id=PROJECT_ID,
        product_id=PRODUCT_ID,
        metadata=KeyMetadata(
            name="Alice's production key",
            scopes=["read", "write"],
            rate_limit=1000,
            custom={"user_id": "u_alice", "plan": "pro"},
        ),
    )
    print(f"plaintext: {result.plaintext}")  # return this ONCE to the user

    # ── Validate an incoming request ─────────────────────────────────────────
    key = await client.validate_key(
        result.plaintext,
        product_id=PRODUCT_ID,
        required_scope="read",
    )
    print(f"user: {key.metadata.custom['user_id']}  plan: {key.metadata.custom['plan']}")

    # ── List a user's keys ───────────────────────────────────────────────────
    all_keys   = await client.list_project_keys(PROJECT_ID)
    alice_keys = [k for k in all_keys if k.metadata.custom.get("user_id") == "u_alice"]
    for k in alice_keys:
        print(f"  {k.metadata.name}  [{k.id}]  revoked={k.revoked_at is not None}")

asyncio.run(main())
```

Source: [`examples/acme_integration.py`](https://github.com/AKSHILMY/api-project/blob/main/examples/acme_integration.py)

## License

MIT
