diff --git a/README.md b/README.md index 98bc51c..5d58e0e 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Details in [`docs/stability/`](./docs/stability) — semver, IR stability, LTS. The official adapter set lives inside [`causeway.contrib`](./packages/causeway/src/causeway/contrib) and installs through extras like `causeway[dramatiq]`, `causeway[s3]`, `causeway[jwt]`, and `causeway[sqlmodel]`. Full inventory and roadmap in [`ROADMAP.md`](./ROADMAP.md#plugin-ecosystem). -> The React story is what I use day to day, so React and Next.js are the bindings I maintain. Solid, Svelte, or anything else can ride on the same `@causewayjs/ts` runtime — if you build one, I'd love to point people at it. +> React and Next.js are the bindings I maintain. Any other framework can ride on the same `@causewayjs/ts` runtime — if you build a binding for one, I'd love to point people at it. ## Contributing diff --git a/commitlint.config.mjs b/commitlint.config.mjs index 51af911..929d36e 100644 --- a/commitlint.config.mjs +++ b/commitlint.config.mjs @@ -12,12 +12,9 @@ export default { "runtime", "codegen", "ir", - "polyglot", "openapi", "streaming", "react", - "solid", - "svelte", "ts", "routing", "config", diff --git a/docs/README.md b/docs/README.md index 3f2d94a..e698b52 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,7 +50,7 @@ The longer design story is [Why Causeway](./why-causeway.md). | Add auth, idempotency, or tenancy | [Permissions](./backend/permissions.md), [idempotency keys](./backend/idempotency.md), [multi-tenancy](./backend/multi-tenant.md) | | Add tasks, events, or webhooks | [Background tasks](./app/tasks.md), [events](./app/events.md), [webhooks](./app/webhooks.md) | | Test routes | [Testing](./app/testing.md), [inline scenarios](./app/inline-scenarios.md) | -| Deploy | [Deploying overview](./deploy/index.md), [Docker](./deploy/docker.md), [Fly.io](./deploy/fly.md), [Modal](./deploy/modal.md) | +| Deploy | [Deploying overview](./deploy/index.md), [Docker](./deploy/docker.md) | ## Core Concepts diff --git a/docs/app/configuration.md b/docs/app/configuration.md index b3770d0..769677e 100644 --- a/docs/app/configuration.md +++ b/docs/app/configuration.md @@ -48,7 +48,6 @@ For request-scoped values that depend on settings (a DB pool, a Stripe client), ```python class Settings(BaseSettings): env: str = "dev" - sentry_dsn: SecretStr | None = None s3_bucket: str = "uploads-local" @property @@ -62,7 +61,6 @@ Per-env activation in `plugins.py`: from causeway import register, env if env() == "prod": - register(SentryObserver(dsn=settings.sentry_dsn.get_secret_value())) register(S3Storage(bucket=settings.s3_bucket)) else: register(LocalStorage(path="./tmp/uploads")) diff --git a/docs/app/observability.md b/docs/app/observability.md index ab97e6f..f777d52 100644 --- a/docs/app/observability.md +++ b/docs/app/observability.md @@ -96,7 +96,7 @@ Disable in production via `create_app(..., diagnostics=False)`. ## What Causeway doesn't do - **Metrics export.** Use the `MetricsSink` contract with a plugin (`causeway-metrics-statsd`, `causeway-metrics-prometheus`). -- **Log shipping.** The `LogSink` contract or just point your container runtime at stdout. +- **Log shipping.** Point your container runtime at stdout. - **APM dashboards.** SigNoz, Honeycomb, Datadog, Tempo — pick one, point OTel at it. The framework wires correlation. The transport and storage are the user's choice. @@ -120,17 +120,6 @@ async def time_request(req): For app-wide timing, install a class `Middleware` at the root. -**Sentry:** - -```python -# src/app/plugins.py -from causeway.contrib.sentry import SentryObserver -from causeway import register, env - -if env() == "prod": - register(SentryObserver(dsn=settings.sentry_dsn.get_secret_value())) -``` - **OTel with auto-instrumentation:** ```python diff --git a/docs/app/plugins.md b/docs/app/plugins.md index 0a340e1..7c8c904 100644 --- a/docs/app/plugins.md +++ b/docs/app/plugins.md @@ -66,13 +66,11 @@ def plugin(settings): from causeway import register, env from causeway.contrib.dramatiq import DramatiqAdapter from causeway.contrib.s3 import S3Storage -from causeway.contrib.sentry import SentryObserver from app.config import settings if env() == "prod": - register(SentryObserver(dsn=settings.sentry_dsn.get_secret_value())) # first → wraps everything - register(DramatiqAdapter(broker_url=settings.redis_url.get_secret_value())) + register(DramatiqAdapter(broker_url=settings.redis_url.get_secret_value())) # first → others can depend on it register(S3Storage(bucket="uploads")) ``` @@ -89,14 +87,10 @@ swap. | `TaskAdapter` | `enqueue`, `schedule`, `cron`, `eager`, `status`, `result` | `causeway.tasks.InMemoryAdapter` | | `Storage` | `put`, `get`, `delete`, `signed_url`, `list` | `causeway.adapters.LocalStorage` | | `KV` | `get`, `set` (TTL), `delete`, `incr`, `expire` | `causeway.adapters.MemoryKV` | -| `SessionStore` | `read`, `write`, `destroy`, `rotate` | `causeway.adapters.CookieStore` | | `Mailer` | `send`, `send_template`, `verify_address` | bring your own | -| `Searchable` | `index`, `search`, `delete`, `bulk_index` | bring your own | | `RateLimiter` | `acquire`, `peek`, `reset` | `causeway.adapters.MemoryLimiter` | | `FeatureFlags` | `is_on`, `variant`, `refresh` | `causeway.adapters.StaticFlags` | | `MetricsSink` | `counter`, `gauge`, `histogram`, `timer` | none | -| `LogSink` | `emit(record)` | stdout via `structlog` | -| `PubSub` | `publish`, `subscribe` | none | | `AuthProvider` | `current_user`, `login`, `logout`, `verify` | bring your own | | `DBSession` | `session`, `transaction`, `health` | provided by ORM adapters | | `BlobScanner` | `scan(stream)` — virus / type checks | none | @@ -150,7 +144,6 @@ from causeway import register, env from app.config import settings if env() == "prod": - register(SentryObserver(dsn=settings.sentry_dsn.get_secret_value())) register(S3Storage(bucket=settings.s3_bucket)) else: register(LocalStorage(path="./tmp/uploads")) @@ -169,7 +162,7 @@ $ causeway plugins │ DramatiqAdapter │ v1.0 │ causeway.contrib.dramatiq │ │ S3Storage │ v1.0 │ causeway.contrib.s3 │ │ RedisCache │ v1.0 │ causeway.contrib.redis │ -│ SmtpMailer │ v1.0 │ causeway.contrib.smtp │ +│ JwtAuth │ v1.0 │ causeway.contrib.jwt │ └──────────────────┴───────────────────┴──────────────────────┘ ``` @@ -183,11 +176,8 @@ Official adapters use short extras and matching `causeway.contrib` modules: - **storage**: `causeway[fs]`, `causeway[s3]` -> `causeway.contrib.fs`, `causeway.contrib.s3` - **cache**: `causeway[redis]` -> `causeway.contrib.redis` - **auth**: `causeway[jwt]` -> `causeway.contrib.jwt` -- **mailer**: `causeway[smtp]` -> `causeway.contrib.smtp` -- **observe**: `causeway[sentry]` -> `causeway.contrib.sentry` -- **flags**: `causeway[growthbook]` -> `causeway.contrib.growthbook` - **db**: `causeway[sqlmodel]` -> `causeway.contrib.sqlmodel` -- **deploy**: `causeway[docker]`, `causeway[fly]`, `causeway[modal]` -> `causeway.contrib.docker`, `causeway.contrib.fly`, `causeway.contrib.modal` +- **deploy**: `causeway[docker]` -> `causeway.contrib.docker` Third-party plugins should use `causeway-contrib-` to avoid implying official status. diff --git a/docs/architecture/ir-flow.md b/docs/architecture/ir-flow.md index d94dba7..971fe9d 100644 --- a/docs/architecture/ir-flow.md +++ b/docs/architecture/ir-flow.md @@ -77,7 +77,7 @@ Each change is classified per [IR stability](../stability/ir-stability.md): ## Why an IR layer at all -So one source of truth feeds more than one tool. Today the primary generator is the TypeScript route-key client. The same IR also feeds compatibility/export surfaces such as OpenAPI 3.1, Swift, and Kotlin. Each generator consumes the same contract instead of re-parsing Python. +So one source of truth feeds more than one tool. Today the primary generator is the TypeScript route-key client. The same IR also feeds compatibility/export surfaces such as OpenAPI 3.1. Each generator consumes the same contract instead of re-parsing Python. It also makes contract-stability tooling tractable. `causeway diff` walks the IR rather than parsing Python — that's the only way you get fast, reliable breaking-change detection in CI. diff --git a/docs/architecture/runtime-substrate.md b/docs/architecture/runtime-substrate.md index f1f2fc7..6829b02 100644 --- a/docs/architecture/runtime-substrate.md +++ b/docs/architecture/runtime-substrate.md @@ -65,9 +65,8 @@ phases — is your framework's call. The IR is the contract. `causeway._runtime.ir.AppIR` is a pure data structure. Walk it however you want. The shipping renderers -(`causeway._runtime.codegen` for TypeScript, -`causeway._runtime.polyglot` for Swift / Kotlin, and -`causeway._runtime.openapi` for OpenAPI 3.1) are all built on the same +(`causeway._runtime.codegen` for TypeScript and +`causeway._runtime.openapi` for OpenAPI 3.1) are both built on the same public IR — your renderer joins the same line. ## The substrate's public surface diff --git a/docs/deploy/binary.md b/docs/deploy/binary.md index 9bce40f..7f8eb5a 100644 --- a/docs/deploy/binary.md +++ b/docs/deploy/binary.md @@ -168,7 +168,6 @@ distroless attack surface gets you ahead of the typical `--assume-yes-for-downloads` (already in the default command). - **Cross-compilation isn't supported.** Build on the same OS/arch you ship to. Use a matrix in CI. -- **Plugins that do runtime importlib magic** (rare, but - `causeway[growthbook]` does some) may need explicit +- **Plugins that do runtime importlib magic** (rare) may need explicit `--include-package` hints. Pass them via the `extra_packages` argument to `build_binary()` or open an issue with the plugin. diff --git a/docs/deploy/fly.md b/docs/deploy/fly.md deleted file mode 100644 index 7541c7c..0000000 --- a/docs/deploy/fly.md +++ /dev/null @@ -1,68 +0,0 @@ -# Deploying to Fly.io - -Via the `causeway[fly]` adapter. - -## Install - -```bash -uv add "causeway[fly]" -brew install flyctl -fly auth login -``` - -## Register - -```python -# src/app/plugins.py -from causeway import register -from causeway.contrib.fly import FlyDeploy - -register(FlyDeploy( - app_name="my-app", - region="iad", - primary_region="iad", -)) -``` - -## Deploy - -```bash -fly apps create my-app # one-time -causeway build -causeway deploy fly -``` - -What that does: - -1. Generates `fly.toml` from the adapter config. -2. Generates a Dockerfile (or reuses an existing one). -3. Calls `fly deploy --remote-only`. - -## Secrets - -Set per-app secrets via `flyctl`: - -```bash -fly secrets set DATABASE_URL=postgres://... CAUSEWAY_ENV=prod -``` - -The app reads these like any other env var; your `Settings` picks them up. - -## Health checks - -`fly.toml` is wired with: - -```toml -[[services.http_checks]] - path = "/healthz" - interval = "10s" - timeout = "2s" -``` - -Override `/readyz` if your readiness depends on more than the default plugin aggregation. - -## See also - -- [Deploying overview](./index.md) -- [`causeway deploy`](../reference/cli/deploy.md) -- [Fly docs](https://fly.io/docs/) diff --git a/docs/deploy/index.md b/docs/deploy/index.md index 4b653e4..8e8b03c 100644 --- a/docs/deploy/index.md +++ b/docs/deploy/index.md @@ -2,11 +2,9 @@ A Causeway app is **an ASGI app + a manifest**. It runs anywhere ASGI runs — Docker, Fly, Modal, Lambda (via Mangum), bare uvicorn behind nginx, your own Kubernetes cluster. -The framework doesn't own deploys. Three official adapters wrap the common targets so you don't have to write Dockerfiles by hand: +The framework doesn't own deploys. The official adapter wraps the common case so you don't have to write Dockerfiles by hand: - **[Docker](./docker.md)** — `causeway[docker]`. Builds an image from your project. -- **[Fly.io](./fly.md)** — `causeway[fly]`. Wraps `flyctl` deploy. -- **[Modal](./modal.md)** — `causeway[modal]`. Wraps the Modal SDK for ephemeral function-as-a-service. - **[Binary export](./binary.md)** — `causeway build --binary`. Single AOT-compiled executable for self-hosting; pairs with `FROM scratch` containers. Every deploy adapter implements the [`DeployTarget`](../reference/classes/contracts.md#deploytarget) contract — `manifest()`, `package()`, `push(target)`. @@ -20,7 +18,7 @@ causeway build # emit dist/ir.json + client/ + wheel causeway deploy # plugin reads dist/ and pushes ``` -The adapter produces the target-specific artifact (Dockerfile + image, Fly machine spec, Modal stub) from the project shape. You don't write target-specific glue. +The adapter produces the target-specific artifact (Dockerfile + image) from the project shape. You don't write target-specific glue. ## What gets deployed @@ -47,7 +45,7 @@ Run with: uvicorn app:app --host 0.0.0.0 --port 8000 ``` -You're done. Causeway emits an ASGI app — it's not Docker-aware, Fly-aware, or Modal-aware in any way. +You're done. Causeway emits an ASGI app — it's not Docker-aware in any way. ## Health checks @@ -69,5 +67,3 @@ You're done. Causeway emits an ASGI app — it's not Docker-aware, Fly-aware, or ## Per-target guides - **[Docker](./docker.md)** -- **[Fly.io](./fly.md)** -- **[Modal](./modal.md)** diff --git a/docs/deploy/modal.md b/docs/deploy/modal.md deleted file mode 100644 index 5a99fa5..0000000 --- a/docs/deploy/modal.md +++ /dev/null @@ -1,72 +0,0 @@ -# Deploying to Modal - -Via the `causeway[modal]` adapter. - -## Install - -```bash -uv add "causeway[modal]" -modal token new -``` - -## Register - -```python -# src/app/plugins.py -from causeway import register -from causeway.contrib.modal import ModalDeploy - -register(ModalDeploy( - name="my-app", - cpu=1.0, - memory_mb=512, -)) -``` - -## Deploy - -```bash -causeway build -causeway deploy modal -``` - -What that does: - -1. Reads the `ModalDeploy` adapter. -2. Generates a Modal stub that exposes the ASGI app via `@modal.asgi_app()`. -3. Calls `modal deploy stub.py`. - -## When Modal makes sense - -- Bursty workloads (occasional API hits, scheduled jobs). -- ML inference endpoints where you want GPU-on-demand. -- Anything where you'd rather not run a server 24/7. - -## When it doesn't - -- Steady-state traffic that fits in a single VM. -- Latency-critical workloads (cold starts in the hundreds of milliseconds). -- Long-lived WebSockets / SSE streams (Modal has a per-invocation timeout). - -## Secrets - -Modal has its own secrets store: - -```bash -modal secret create my-secrets DATABASE_URL=postgres://... CAUSEWAY_ENV=prod -``` - -Mount in the stub: - -```python -import modal -secrets = [modal.Secret.from_name("my-secrets")] -``` - -The plugin wires this for you when you list secret names in the adapter config. - -## See also - -- [Deploying overview](./index.md) -- [`causeway deploy`](../reference/cli/deploy.md) -- [Modal docs](https://modal.com/docs) diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index 2e1de1c..1060f31 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -85,9 +85,8 @@ Two discovery paths: credentials). A plugin implements one or more **contracts**: `TaskAdapter`, -`Storage`, `KV`, `SessionStore`, `Mailer`, `Searchable`, `RateLimiter`, -`FeatureFlags`, `MetricsSink`, `LogSink`, `PubSub`, `AuthProvider`, -`DBSession`, … Each contract ships with a reference adapter in core (or +`Storage`, `KV`, `Mailer`, `RateLimiter`, `FeatureFlags`, `MetricsSink`, +`AuthProvider`, `DBSession`, … Each contract ships with a reference adapter in core (or in a sibling repo for plugins that need a real dependency). Picking a real backend is a one-line swap. Full mechanics in [`plugins.md`](../app/plugins.md). diff --git a/docs/internals/code-map.md b/docs/internals/code-map.md index 888d747..c2e8c65 100644 --- a/docs/internals/code-map.md +++ b/docs/internals/code-map.md @@ -83,9 +83,9 @@ Builds the App Graph from the runtime app: routes, route keys, HTTP paths, sourc ## Plugins -### `contracts.py` — 363 lines +### `contracts.py` — 296 lines -Every official contract as a `typing.Protocol`. `Plugin`, `TaskAdapter`, `EventBus`, `Storage`, `KV`, `Mailer`, `AuthProvider`, `DBSession`, `BlobScanner`, `FeatureFlags`, `MetricsSink`, `LogSink`, `PubSub`, `RateLimiter`, `SessionStore`, `DeployTarget`. Each contract declares `contract_version: ClassVar[str] = "v1.0"`. +Every official contract as a `typing.Protocol`. `Plugin`, `TaskAdapter`, `EventBus`, `Storage`, `KV`, `Mailer`, `AuthProvider`, `DBSession`, `BlobScanner`, `FeatureFlags`, `MetricsSink`, `RateLimiter`, `DeployTarget`. Each contract declares `contract_version: ClassVar[str] = "v1.0"`. If you're adding a new official contract, **start here**. Then add a reference adapter in `adapters.py` or an optional adapter under `contrib/`, plus a test @@ -100,9 +100,9 @@ The registry. Two discovery paths feed one ordered dict: Lifecycle: `startup_all(settings)` fires every plugin's `startup` in registration order; `shutdown_all()` fires `shutdown` in reverse. `check_required_contracts()` walks `requires` lists and refuses to boot if a dependency is missing. `merge_settings_fragments(settings)` applies each plugin's optional `settings_fragment()` to the `Settings` instance. -### `adapters.py` — 305 lines +### `adapters.py` — 190 lines -Reference adapters shipped in core: `MemoryKV`, `LocalStorage`, `MemoryLimiter`, `StaticFlags`, `NullSink`, `MemoryBus`, `NullScanner`. These exist so the framework boots out of the box; production users swap them via a sibling `causeway-*` package. +Reference adapters shipped in core: `MemoryKV`, `LocalStorage`, `MemoryLimiter`, `StaticFlags`, `NullSink`, `NullScanner`. These exist so the framework boots out of the box; production users swap them via a sibling `causeway-*` package. --- @@ -186,9 +186,9 @@ The inline-scenario runtime. Lives in a private subpackage so the implementation Registered via the `pytest11` entry point. Adds `--causeway-routes`, `--update-snapshots`, `--causeway-no-inline`; matches route files via `pytest_collect_file`; yields one `ScenarioItem` per `scenario(...)` block; applies snapshot rewrites in `pytest_sessionfinish`. -### `cli.py` — 543 lines +### `cli.py` — 501 lines -The `causeway` CLI built on Typer. Commands: `new` (scaffold via `_scaffold.py`), `dev` (owned uvicorn server + smart route hot-swap), `build` (codegen + wheel), `codegen`, `ir`, `inspect` (App Graph), `freeze`, auxiliary generators (`openapi`, `swift`, `kotlin`), `plugins` (list registered adapters), `diff` (IR breaking-change detection), `deploy ` (dispatch to a registered `DeployTarget`), and `plugin new ` (scaffold a new plugin package). +The `causeway` CLI built on Typer. Commands: `new` (scaffold via `_scaffold.py`), `dev` (owned uvicorn server + smart route hot-swap), `build` (codegen + wheel), `codegen`, `ir`, `inspect` (App Graph), `freeze`, `openapi` (auxiliary generator), `plugins` (list registered adapters), `diff` (IR breaking-change detection), `deploy ` (dispatch to a registered `DeployTarget`), and `plugin new ` (scaffold a new plugin package). ### `_scaffold.py` — 290 lines diff --git a/docs/reference/classes/contracts.md b/docs/reference/classes/contracts.md index e56708e..dea0f83 100644 --- a/docs/reference/classes/contracts.md +++ b/docs/reference/classes/contracts.md @@ -10,14 +10,10 @@ from causeway.contracts import ( WebhookStore, Storage, KV, - SessionStore, Mailer, - PubSub, RateLimiter, FeatureFlags, MetricsSink, - LogSink, - Searchable, DBSession, AuthProvider, BlobScanner, @@ -138,20 +134,6 @@ Reference: `causeway.adapters.MemoryKV`. Real adapters: `causeway[redis]`. --- -## `SessionStore` - -```python -class SessionStore(Plugin, Protocol): - async def read(self, session_id: str) -> dict[str, Any] | None: ... - async def write(self, session_id: str, data: dict[str, Any]) -> None: ... - async def destroy(self, session_id: str) -> None: ... - async def rotate(self, session_id: str) -> str: ... -``` - -Reference: `causeway.adapters.CookieStore`. - ---- - ## `Mailer` ```python @@ -161,17 +143,7 @@ class Mailer(Plugin, Protocol): async def verify_address(self, address: str) -> bool: ... ``` -Real adapters: `causeway[smtp]`. No in-core reference. - ---- - -## `PubSub` - -```python -class PubSub(Plugin, Protocol): - async def publish(self, topic: str, payload: bytes) -> None: ... - async def subscribe(self, topic: str, handler: Callable[[bytes], Awaitable[None]]) -> None: ... -``` +No in-core reference; bring your own. --- @@ -197,7 +169,7 @@ class FeatureFlags(Plugin, Protocol): async def refresh(self) -> None: ... ``` -Reference: `causeway.adapters.StaticFlags` (reads `Settings.feature_flags`). Real adapters: `causeway[growthbook]`. +Reference: `causeway.adapters.StaticFlags` (reads `Settings.feature_flags`). --- @@ -213,29 +185,6 @@ class MetricsSink(Plugin, Protocol): --- -## `LogSink` - -```python -class LogSink(Plugin, Protocol): - def emit(self, record: dict[str, Any]) -> None: ... -``` - -Reference: stdout via `structlog`. - ---- - -## `Searchable` - -```python -class Searchable(Plugin, Protocol): - async def index(self, doc_id: str, doc: dict[str, Any]) -> None: ... - async def search(self, query: str, *, limit: int = 20) -> list[dict[str, Any]]: ... - async def delete(self, doc_id: str) -> None: ... - async def bulk_index(self, docs: list[tuple[str, dict[str, Any]]]) -> None: ... -``` - ---- - ## `DBSession` ```python @@ -281,7 +230,7 @@ class DeployTarget(Plugin, Protocol): async def push(self, target: str) -> str: ... ``` -Real adapters: `causeway[docker]`, `causeway[fly]`, `causeway[modal]`. +Real adapters: `causeway[docker]`. --- diff --git a/docs/reference/cli/deploy.md b/docs/reference/cli/deploy.md index 5f9acd5..29fc93e 100644 --- a/docs/reference/cli/deploy.md +++ b/docs/reference/cli/deploy.md @@ -4,8 +4,6 @@ Invoke the relevant `DeployTarget` plugin. ```bash causeway deploy docker -causeway deploy fly -causeway deploy modal ``` ## Synopsis @@ -26,8 +24,6 @@ causeway deploy [--output ] The CLI scans registered plugins for one whose class name matches `Deploy` (case-insensitive). For example: - `causeway deploy docker` → looks for a `DockerDeploy` adapter. -- `causeway deploy fly` → looks for a `FlyDeploy` adapter. -- `causeway deploy modal` → looks for a `ModalDeploy` adapter. If no adapter matches, the command fails with: diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index 42ad665..8b322be 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -15,8 +15,6 @@ Commands: inspect Inspect the App Graph. freeze Emit the AOT build tree. openapi Emit OpenAPI 3.1 JSON. - swift Emit a Swift client. - kotlin Emit a Kotlin client. plugins List registered plugin adapters. plugin new Scaffold a new plugin package. diff Compare two IR snapshots. @@ -36,8 +34,6 @@ Commands: | `causeway inspect` | [`inspect`](./inspect.md) | | `causeway freeze` | [`freeze`](./freeze.md) | | `causeway openapi` | [`openapi`](./openapi.md) | -| `causeway swift` | [`swift`](./swift.md) | -| `causeway kotlin` | [`kotlin`](./kotlin.md) | | `causeway plugins` | [`plugins`](./plugins.md) | | `causeway plugin new ` | [`plugin new`](./plugin-new.md) | | `causeway diff ` | [`diff`](./diff.md) | diff --git a/docs/reference/cli/kotlin.md b/docs/reference/cli/kotlin.md deleted file mode 100644 index 72e89eb..0000000 --- a/docs/reference/cli/kotlin.md +++ /dev/null @@ -1,26 +0,0 @@ -# `causeway kotlin` - -Generate a Kotlin client from the route IR. - -```bash -causeway kotlin app:app --out Causeway.kt -``` - -## Synopsis - -``` -causeway kotlin [module] [--out ] -``` - -## Options - -| Option | Default | Description | -| -------------- | ------------- | ------------------- | -| `--out` / `-o` | `Causeway.kt` | Output Kotlin file. | - -Kotlin generation is an auxiliary export. The canonical JavaScript/TypeScript surface is still the generated route-key client. - -## See Also - -- [`ir`](./ir.md) -- [IR stability](../../stability/ir-stability.md) diff --git a/docs/reference/cli/swift.md b/docs/reference/cli/swift.md deleted file mode 100644 index df8d53d..0000000 --- a/docs/reference/cli/swift.md +++ /dev/null @@ -1,26 +0,0 @@ -# `causeway swift` - -Generate a Swift client from the route IR. - -```bash -causeway swift app:app --out Causeway.swift -``` - -## Synopsis - -``` -causeway swift [module] [--out ] -``` - -## Options - -| Option | Default | Description | -| -------------- | ---------------- | ------------------ | -| `--out` / `-o` | `Causeway.swift` | Output Swift file. | - -Swift generation is an auxiliary export. The canonical JavaScript/TypeScript surface is still the generated route-key client. - -## See Also - -- [`ir`](./ir.md) -- [IR stability](../../stability/ir-stability.md) diff --git a/docs/reference/functions/env.md b/docs/reference/functions/env.md index 7e6d201..e1a8485 100644 --- a/docs/reference/functions/env.md +++ b/docs/reference/functions/env.md @@ -27,7 +27,7 @@ env() -> str from causeway import register, env if env() == "prod": - register(SentryObserver(dsn=...)) + register(S3Storage(bucket=...)) ``` > **Good to know.** Causeway doesn't validate the value — anything in the env var goes. If you want to constrain it (`dev` / `staging` / `prod`), add a `field_validator` to your `Settings` class. diff --git a/docs/reference/functions/register.md b/docs/reference/functions/register.md index 7eb75d2..4d004c5 100644 --- a/docs/reference/functions/register.md +++ b/docs/reference/functions/register.md @@ -41,7 +41,6 @@ function calls `register()` for you — see [Plugin authoring](../../app/plugin- from causeway import register, env if env() == "prod": - register(SentryObserver(dsn=...)) register(S3Storage(bucket="...")) else: register(LocalStorage(path="./tmp/uploads")) diff --git a/docs/reference/index.md b/docs/reference/index.md index 45e4a5f..1d36f73 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -75,8 +75,6 @@ Every public symbol Causeway exports, on its own page. Organized by kind: decora | [`causeway inspect`](./cli/inspect.md) | Inspect the App Graph. | | [`causeway freeze`](./cli/freeze.md) | Emit the AOT build tree without compiling. | | [`causeway openapi`](./cli/openapi.md) | Emit OpenAPI 3.1 JSON for non-Causeway consumers. | -| [`causeway swift`](./cli/swift.md) | Emit a Swift client from the route IR. | -| [`causeway kotlin`](./cli/kotlin.md) | Emit a Kotlin client from the route IR. | | [`causeway plugins`](./cli/plugins.md) | List registered plugin adapters. | | [`causeway plugin new `](./cli/plugin-new.md) | Scaffold a new plugin package. | | [`causeway diff `](./cli/diff.md) | Compare two IR snapshots and flag breaking changes. | diff --git a/package.json b/package.json index 7b12c0b..a9afea1 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "url": "git+https://github.com/ruwadgroup/causeway.git" }, "workspaces": [ - "packages/*", - "examples/*/frontend" + "packages/*" ], "scripts": { "build": "pnpm -r --filter=./packages/causeway-* run build", diff --git a/packages/causeway/pyproject.toml b/packages/causeway/pyproject.toml index c257d7c..58b493c 100644 --- a/packages/causeway/pyproject.toml +++ b/packages/causeway/pyproject.toml @@ -56,11 +56,6 @@ sqlmodel = [ "sqlmodel>=0.0.22", ] docker = [] -fly = [] -modal = ["modal>=0.65"] -growthbook = ["growthbook>=1.0", "httpx>=0.28"] -smtp = ["aiosmtplib>=3.0"] -sentry = ["sentry-sdk>=2.0"] fs = [] s3 = ["aioboto3>=13.0"] dramatiq = ["dramatiq[redis]>=2.1.0", "periodiq>=0.14.0"] @@ -72,7 +67,7 @@ otel = [ ] binary = ["nuitka>=2.5"] all = [ - "causeway[jwt,redis,sqlmodel,docker,fly,modal,growthbook,smtp,sentry,fs,s3,dramatiq,otel,binary]", + "causeway[jwt,redis,sqlmodel,docker,fs,s3,dramatiq,otel,binary]", ] [project.scripts] diff --git a/packages/causeway/src/causeway/_runtime/_idents.py b/packages/causeway/src/causeway/_runtime/_idents.py index f54a324..73d9470 100644 --- a/packages/causeway/src/causeway/_runtime/_idents.py +++ b/packages/causeway/src/causeway/_runtime/_idents.py @@ -1,4 +1,4 @@ -"""Identifier transforms shared between codegen and polyglot renderers.""" +"""Identifier transforms used by the codegen renderer.""" from __future__ import annotations diff --git a/packages/causeway/src/causeway/_runtime/ir.py b/packages/causeway/src/causeway/_runtime/ir.py index 033f50d..55215a8 100644 --- a/packages/causeway/src/causeway/_runtime/ir.py +++ b/packages/causeway/src/causeway/_runtime/ir.py @@ -1,9 +1,8 @@ """IR builder. Every registered handler reduces to a JSON-Schema-2020-12 IR that the -codegen walks to emit TypeScript. The two-step (IR → renderer) split is -what lets polyglot clients (Swift, Kotlin) follow later without rewriting -type extraction. +codegen walks to emit TypeScript. The two-step (IR → renderer) split keeps +type extraction independent of the renderer. Two passes: diff --git a/packages/causeway/src/causeway/_runtime/polyglot.py b/packages/causeway/src/causeway/_runtime/polyglot.py deleted file mode 100644 index 9c5ae29..0000000 --- a/packages/causeway/src/causeway/_runtime/polyglot.py +++ /dev/null @@ -1,770 +0,0 @@ -"""Polyglot client codegen — Swift and Kotlin renderers off the same IR. - -Both renderers emit a working HTTP client: typed args, typed responses, -typed ``@raises`` error unions (as sealed enums / nested enums), and -JSON encoding/decoding that matches causeway's snake_case wire format. - -Streaming endpoints surface as language-native async streams: -- Swift: ``AsyncThrowingStream`` backed by - ``URLSession.bytes(for:)`` with an inline SSE parser. -- Kotlin: ``kotlinx.coroutines.flow.Flow`` backed by - ``HttpURLConnection.inputStream`` and an inline SSE parser, dispatched - on ``Dispatchers.IO``. - -Both honor the SSE ``event: done`` / ``event: error`` conventions. - -Scope notes: - -- Swift uses URLSession + JSONEncoder/JSONDecoder. No SwiftNIO dep. -- Kotlin uses HttpURLConnection + kotlinx.serialization + kotlinx.coroutines. - No ktor / OkHttp dependency — keep the generated file drop-in for any - Gradle module that already depends on kotlinx.serialization. -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any, cast - -from causeway._runtime._idents import to_camel as _to_camel -from causeway._runtime.ir import AppIR, RouteIR - -_PRIMITIVES = {"string", "integer", "number", "boolean", "null"} - - -def _ref_name(schema: dict[str, Any]) -> str | None: - ref = schema.get("$ref") - if isinstance(ref, str): - return ref.rsplit("/", 1)[-1] - return None - - -def _is_null_schema(value: Any) -> bool: - return isinstance(value, dict) and cast("dict[str, Any]", value).get("type") == "null" - - -_SWIFT_PREAMBLE = """\ -// AUTO-GENERATED by `causeway`. Do not edit by hand. Re-run `causeway swift`. - -import Foundation - -public struct CausewayClient { - public let baseURL: URL - public var defaultHeaders: [String: String] - public let session: URLSession - - public init( - baseURL: URL, - defaultHeaders: [String: String] = [:], - session: URLSession = .shared - ) { - self.baseURL = baseURL - self.defaultHeaders = defaultHeaders - self.session = session - } - - fileprivate static let encoder: JSONEncoder = { - let e = JSONEncoder() - e.keyEncodingStrategy = .convertToSnakeCase - return e - }() - - fileprivate static let decoder: JSONDecoder = { - let d = JSONDecoder() - d.keyDecodingStrategy = .convertFromSnakeCase - return d - }() -} - -public enum CausewayRPCError: Error { - case http(status: Int, body: Data) - case decode(Error) - case transport(Error) -} - -public enum CausewayResult { - case ok(Success) - case err(Failure) -} -""" - - -def render_swift(ir: AppIR) -> str: - parts: list[str] = [_SWIFT_PREAMBLE] - for name in sorted(ir.components): - parts.append(_swift_struct(name.rsplit(".", 1)[-1], ir.components[name])) - - for method_name, variants in _collect_error_unions(ir).items(): - parts.append(_swift_error_enum(method_name, variants)) - - parts.append("public extension CausewayClient {\n") - for route in ir.routes: - parts.append(_swift_method(route)) - parts.append("}\n") - return "".join(parts) - - -def write_swift(ir: AppIR, out: Path) -> None: - out.parent.mkdir(parents=True, exist_ok=True) - out.write_text(render_swift(ir), encoding="utf-8") - - -def _swift_struct(name: str, schema: dict[str, Any]) -> str: - props_any: Any = schema.get("properties") - if not isinstance(props_any, dict): - return "" - props = cast("dict[str, Any]", props_any) - required = cast("list[str]", schema.get("required") or []) - ident = _swift_ident(name) - lines = [f"public struct {ident}: Codable {{\n"] - for raw_name, child in props.items(): - is_required = raw_name in required - camel = _to_camel(raw_name) - ty = _swift_type(child) - if not is_required: - ty = f"{ty}?" - lines.append(f" public let {camel}: {ty}\n") - lines.append("}\n\n") - return "".join(lines) - - -def _swift_error_enum(method_name: str, variants: list[str]) -> str: - enum_name = _swift_error_enum_name(method_name) - out = [f"public enum {enum_name}: Error {{\n"] - for v in variants: - out.append(f" case {_lower_first(v)}({_swift_ident(v)})\n") - out.append("\n static func decode(kind: String, data: Data) throws -> ") - out.append(f"{enum_name} {{\n") - out.append(" switch kind {\n") - for v in variants: - out.append( - f' case "{v}": return .{_lower_first(v)}(try CausewayClient.decoder.decode' - ) - out.append(f"({_swift_ident(v)}.self, from: data))\n") - out.append( - ' default: throw CausewayRPCError.decode(NSError(domain: "causeway", code: 0))\n' - ) - out.append(" }\n") - out.append(" }\n") - out.append("}\n\n") - return "".join(out) - - -def _swift_method(route: RouteIR) -> str: - method = _swift_ident(_to_camel(route.name)) - args = [(_to_camel(p.name), _swift_type(p.schema)) for p in route.params] - body_params = [p for p in route.params if p.location == "body"] - has_raises = bool(route.raises) - ret_ty = _swift_response_type(route) - - arg_list = ", ".join(f"{n}: {t}" for n, t in args) - if route.streams: - return _swift_stream_method(route, method, arg_list, ret_ty) - - head = f" func {method}({arg_list}) async throws -> {ret_ty} {{\n" - - body: list[str] = [head] - body.append(f' var path = "{route.path}"\n') - for p in route.params: - if p.location == "path": - body.append( - f' path = path.replacingOccurrences(of: "{{{p.alias}}}", ' - f'with: "\\({_to_camel(p.name)})")\n' - ) - body.append(" var components = URLComponents(\n") - body.append(" url: baseURL.appendingPathComponent(path),\n") - body.append(" resolvingAgainstBaseURL: false\n") - body.append(" )!\n") - - queries = [p for p in route.params if p.location == "query"] - if queries: - body.append(" var items: [URLQueryItem] = []\n") - for p in queries: - body.append( - f' items.append(URLQueryItem(name: "{p.alias}", ' - f'value: "\\({_to_camel(p.name)})"))\n' - ) - body.append(" components.queryItems = items\n") - - body.append(" var req = URLRequest(url: components.url!)\n") - body.append(f' req.httpMethod = "{route.method}"\n') - body.append(" for (k, v) in defaultHeaders { req.setValue(v, forHTTPHeaderField: k) }\n") - for p in route.params: - if p.location == "header": - body.append( - f' req.setValue("\\({_to_camel(p.name)})", ' - f'forHTTPHeaderField: "{p.alias}")\n' - ) - elif p.location == "cookie": - body.append( - f' req.setValue("{p.alias}=\\({_to_camel(p.name)})", ' - f'forHTTPHeaderField: "Cookie")\n' - ) - - if body_params: - if len(body_params) == 1 and not body_params[0].embed: - payload = _to_camel(body_params[0].name) - body.append( - ' req.setValue("application/json", forHTTPHeaderField: "Content-Type")\n' - ) - body.append(f" req.httpBody = try CausewayClient.encoder.encode({payload})\n") - else: - body.append(" struct _Body: Encodable {\n") - for p in body_params: - body.append(f" let {_to_camel(p.name)}: {_swift_type(p.schema)}\n") - body.append(" }\n") - ctor = ", ".join(f"{_to_camel(p.name)}: {_to_camel(p.name)}" for p in body_params) - body.append(f" let _b = _Body({ctor})\n") - body.append( - ' req.setValue("application/json", forHTTPHeaderField: "Content-Type")\n' - ) - body.append(" req.httpBody = try CausewayClient.encoder.encode(_b)\n") - - body.append(" let (data, response) = try await session.data(for: req)\n") - body.append(" guard let http = response as? HTTPURLResponse else {\n") - body.append( - ' throw CausewayRPCError.transport(NSError(domain: "causeway", code: 0))\n' - ) - body.append(" }\n") - body.append(" guard (200..<300).contains(http.statusCode) else {\n") - body.append(" throw CausewayRPCError.http(status: http.statusCode, body: data)\n") - body.append(" }\n") - - if has_raises: - enum_name = _swift_error_enum_name(route.name) - success_ty = _swift_response_type(route) - body.append(" struct _Env: Decodable { let ok: Bool; let kind: String? }\n") - body.append( - " let env = (try? CausewayClient.decoder.decode(_Env.self, from: data)) ?? _Env(ok: true, kind: nil)\n" - ) - body.append(" if !env.ok {\n") - body.append(" struct _Err: Decodable { let error: [String: AnyCodable] }\n") - body.append(" throw try {() -> Error in\n") - body.append( - " let raw = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n" - ) - body.append(' let errBlob = raw?["error"] as? [String: Any] ?? [:]\n') - body.append(' let kind = errBlob["kind"] as? String ?? ""\n') - body.append( - " let payload = try JSONSerialization.data(withJSONObject: errBlob)\n" - ) - body.append(f" return try {enum_name}.decode(kind: kind, data: payload)\n") - body.append(" }()\n") - body.append(" }\n") - body.append(" struct _Ok: Decodable { let data: " + success_ty + " }\n") - body.append(" let ok = try CausewayClient.decoder.decode(_Ok.self, from: data)\n") - body.append(" return .ok(ok.data)\n") - else: - success_ty = _swift_response_type(route) - if success_ty == "Void": - body.append(" _ = data\n") - body.append(" return\n") - else: - body.append( - f" return try CausewayClient.decoder.decode({success_ty}.self, from: data)\n" - ) - - body.append(" }\n\n") - return "".join(body) - - -def _swift_response_type(route: RouteIR) -> str: - if route.streams: - event = _swift_type(route.event_schema) if route.event_schema else "Data" - return f"AsyncThrowingStream<{event}, Error>" - if route.response is None: - return "Void" - return _swift_type(route.response) - - -def _swift_stream_method(route: RouteIR, method: str, arg_list: str, ret_ty: str) -> str: - """Emit a streaming endpoint as an AsyncThrowingStream backed by URLSession.bytes.""" - event_ty = _swift_type(route.event_schema) if route.event_schema else "Data" - out: list[str] = [] - out.append(f" func {method}({arg_list}) async throws -> {ret_ty} {{\n") - out.append(f' var path = "{route.path}"\n') - for p in route.params: - if p.location == "path": - out.append( - f' path = path.replacingOccurrences(of: "{{{p.alias}}}", ' - f'with: "\\({_to_camel(p.name)})")\n' - ) - out.append(" var components = URLComponents(\n") - out.append(" url: baseURL.appendingPathComponent(path),\n") - out.append(" resolvingAgainstBaseURL: false\n") - out.append(" )!\n") - queries = [p for p in route.params if p.location == "query"] - if queries: - out.append(" var items: [URLQueryItem] = []\n") - for p in queries: - out.append( - f' items.append(URLQueryItem(name: "{p.alias}", ' - f'value: "\\({_to_camel(p.name)})"))\n' - ) - out.append(" components.queryItems = items\n") - - out.append(" var req = URLRequest(url: components.url!)\n") - out.append(f' req.httpMethod = "{route.method}"\n') - out.append(' req.setValue("text/event-stream", forHTTPHeaderField: "Accept")\n') - out.append(" for (k, v) in defaultHeaders { req.setValue(v, forHTTPHeaderField: k) }\n") - for p in route.params: - if p.location == "header": - out.append( - f' req.setValue("\\({_to_camel(p.name)})", ' - f'forHTTPHeaderField: "{p.alias}")\n' - ) - - out.append(" let (bytes, response) = try await session.bytes(for: req)\n") - out.append(" guard let http = response as? HTTPURLResponse,\n") - out.append(" (200..<300).contains(http.statusCode) else {\n") - out.append( - ' throw CausewayRPCError.transport(NSError(domain: "causeway", code: 0))\n' - ) - out.append(" }\n") - out.append(f" return AsyncThrowingStream<{event_ty}, Error> {{ continuation in\n") - out.append(" Task {\n") - out.append(" do {\n") - out.append(' var dataBuf = ""\n') - out.append(" var eventName: String? = nil\n") - out.append(" for try await line in bytes.lines {\n") - out.append(" if line.isEmpty {\n") - out.append(" if !dataBuf.isEmpty {\n") - out.append(' if eventName == "done" {\n') - out.append(" continuation.finish(); return\n") - out.append(" }\n") - out.append(' if eventName == "error" {\n') - out.append( - " let payload = dataBuf.data(using: .utf8) ?? Data()\n" - ) - out.append(" continuation.finish(\n") - out.append(" throwing: CausewayRPCError.http(\n") - out.append( - " status: http.statusCode, body: payload\n" - ) - out.append(" )\n") - out.append(" )\n") - out.append(" return\n") - out.append(" }\n") - out.append(" if let raw = dataBuf.data(using: .utf8) {\n") - out.append( - f" let event = try CausewayClient.decoder.decode({event_ty}.self, from: raw)\n" - ) - out.append(" continuation.yield(event)\n") - out.append(" }\n") - out.append(" }\n") - out.append(' dataBuf = ""\n') - out.append(" eventName = nil\n") - out.append(' } else if line.hasPrefix("event: ") {\n') - out.append(' eventName = String(line.dropFirst("event: ".count))\n') - out.append(' } else if line.hasPrefix("data: ") {\n') - out.append(' if !dataBuf.isEmpty { dataBuf.append("\\n") }\n') - out.append( - ' dataBuf.append(String(line.dropFirst("data: ".count)))\n' - ) - out.append(" }\n") - out.append(" }\n") - out.append(" continuation.finish()\n") - out.append(" } catch {\n") - out.append(" continuation.finish(throwing: error)\n") - out.append(" }\n") - out.append(" }\n") - out.append(" }\n") - out.append(" }\n\n") - return "".join(out) - - -def _swift_error_enum_name(method_name: str) -> str: - return _swift_ident(_to_camel(method_name)[:1].upper() + _to_camel(method_name)[1:]) + "Error" - - -def _collect_error_unions(ir: AppIR) -> dict[str, list[str]]: - out: dict[str, list[str]] = {} - for route in ir.routes: - if route.raises: - out[route.name] = [e.name for e in route.raises] - return out - - -def _swift_type(schema: Any) -> str: - if not isinstance(schema, dict): - return "AnyCodable" - s = cast("dict[str, Any]", schema) - ref = _ref_name(s) - if ref is not None: - return _swift_ident(ref) - t = s.get("type") - if isinstance(t, list): - # nullable: ["string", "null"] - non_null_t = [x for x in cast("list[Any]", t) if x != "null"] # type: ignore[redundant-cast] - if non_null_t: - inner = _swift_type( - {"type": non_null_t[0], **{k: v for k, v in s.items() if k != "type"}} - ) - return f"{inner}?" - return "AnyCodable?" - if t == "string": - return "String" - if t == "integer": - return "Int" - if t == "number": - return "Double" - if t == "boolean": - return "Bool" - if t == "array": - items = s.get("items") - return f"[{_swift_type(items)}]" - if "anyOf" in s or "oneOf" in s: - variants = cast("list[Any]", s.get("anyOf") or s.get("oneOf")) - non_null_v: list[Any] = [v for v in variants if not _is_null_schema(v)] - if len(non_null_v) == 1: - return f"{_swift_type(non_null_v[0])}?" - return "AnyCodable" - return "AnyCodable" - - -def _swift_ident(name: str) -> str: - cleaned = "".join(c if c.isalnum() or c == "_" else "_" for c in name) - return cleaned or "_" - - -def _lower_first(name: str) -> str: - return name[:1].lower() + name[1:] if name else name - - -_KOTLIN_PREAMBLE_TEMPLATE = """\ -// AUTO-GENERATED by `causeway`. Do not edit by hand. Re-run `causeway kotlin`. - -package {package} - -import java.net.HttpURLConnection -import java.net.URL -import java.net.URLEncoder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerialName -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive - -internal val causewayJson = Json {{ ignoreUnknownKeys = true }} - -class CausewayRPCError(val status: Int, val body: String) : RuntimeException("HTTP $status") - -class CausewayClient( - private val baseUrl: String, - private val defaultHeaders: Map = emptyMap(), -) {{ - internal suspend fun request( - method: String, - path: String, - query: List> = emptyList(), - headers: Map = emptyMap(), - body: String? = null, - ): String = withContext(Dispatchers.IO) {{ - val qs = if (query.isEmpty()) "" else "?" + query.joinToString("&") {{ (k, v) -> - URLEncoder.encode(k, "UTF-8") + "=" + URLEncoder.encode(v, "UTF-8") - }} - val url = URL(baseUrl.trimEnd('/') + path + qs) - val conn = (url.openConnection() as HttpURLConnection).apply {{ - requestMethod = method - for ((k, v) in defaultHeaders) setRequestProperty(k, v) - for ((k, v) in headers) setRequestProperty(k, v) - if (body != null) {{ - doOutput = true - setRequestProperty("Content-Type", "application/json") - outputStream.use {{ it.write(body.toByteArray(Charsets.UTF_8)) }} - }} - }} - val status = conn.responseCode - val stream = if (status in 200..299) conn.inputStream else conn.errorStream - val text = stream?.bufferedReader()?.use {{ it.readText() }} ?: "" - if (status !in 200..299) throw CausewayRPCError(status, text) - text - }} -""" - -_KOTLIN_END = "}\n" - - -def render_kotlin(ir: AppIR, *, package: str = "com.causeway.generated") -> str: - parts: list[str] = [_KOTLIN_PREAMBLE_TEMPLATE.format(package=package)] - # Methods live inside the CausewayClient class, so we close it after methods. - - for route in ir.routes: - parts.append(_kotlin_method(route)) - - parts.append(_KOTLIN_END) - - # Data classes + sealed-class error unions go after. - for name in sorted(ir.components): - parts.append(_kotlin_data_class(name.rsplit(".", 1)[-1], ir.components[name])) - - for method_name, variants in _collect_error_unions(ir).items(): - parts.append(_kotlin_error_sealed(method_name, variants)) - - return "".join(parts) - - -def write_kotlin(ir: AppIR, out: Path, *, package: str = "com.causeway.generated") -> None: - out.parent.mkdir(parents=True, exist_ok=True) - out.write_text(render_kotlin(ir, package=package), encoding="utf-8") - - -def _kotlin_data_class(name: str, schema: dict[str, Any]) -> str: - props_any: Any = schema.get("properties") - if not isinstance(props_any, dict): - return "" - props = cast("dict[str, Any]", props_any) - required = cast("list[str]", schema.get("required") or []) - ident = _kotlin_ident(name) - fields_out: list[str] = [] - for raw_name, child in props.items(): - is_required = raw_name in required - camel = _to_camel(raw_name) - ty = _kotlin_type(child) - if not is_required: - ty = f"{ty}? = null" - ann = f'@SerialName("{raw_name}") ' if camel != raw_name else "" - fields_out.append(f" {ann}val {camel}: {ty}") - body = ",\n".join(fields_out) - return f"@Serializable\ndata class {ident}(\n{body}\n)\n\n" - - -def _kotlin_error_sealed(method_name: str, variants: list[str]) -> str: - name = _kotlin_error_enum_name(method_name) - out = [f"sealed class {name} : RuntimeException() {{\n"] - for v in variants: - out.append( - f" data class Is{_kotlin_ident(v)}(val payload: {_kotlin_ident(v)}) : {name}()\n" - ) - out.append("}\n\n") - return "".join(out) - - -def _kotlin_method(route: RouteIR) -> str: - method = _kotlin_ident(_to_camel(route.name)) - arg_list = ", ".join(f"{_to_camel(p.name)}: {_kotlin_type(p.schema)}" for p in route.params) - - if route.streams: - return _kotlin_stream_method(route, method, arg_list) - - return_ty = _kotlin_response_type(route) - if route.raises: - return_ty += f" /* may throw {_kotlin_error_enum_name(route.name)} */" - - out: list[str] = [] - out.append(f" suspend fun {method}({arg_list}): {return_ty} {{\n") - - out.append(f' var path = "{route.path}"\n') - for p in route.params: - if p.location == "path": - out.append( - f' path = path.replace("{{{p.alias}}}", {_to_camel(p.name)}.toString())\n' - ) - - queries = [p for p in route.params if p.location == "query"] - if queries: - out.append(" val query = listOf(\n") - for p in queries: - out.append(f' "{p.alias}" to {_to_camel(p.name)}.toString(),\n') - out.append(" )\n") - else: - out.append(" val query = emptyList>()\n") - - headers = [p for p in route.params if p.location == "header"] - cookies = [p for p in route.params if p.location == "cookie"] - if headers or cookies: - out.append(" val headers = buildMap {\n") - for p in headers: - out.append(f' put("{p.alias}", {_to_camel(p.name)}.toString())\n') - for p in cookies: - out.append( - f' put("Cookie", "{p.alias}=" + {_to_camel(p.name)}.toString())\n' - ) - out.append(" }\n") - else: - out.append(" val headers = emptyMap()\n") - - body_params = [p for p in route.params if p.location == "body"] - if body_params: - if len(body_params) == 1 and not body_params[0].embed: - payload = _to_camel(body_params[0].name) - out.append(f" val body = causewayJson.encodeToString({payload})\n") - else: - entries = ", ".join( - f'"{p.alias}" to causewayJson.encodeToJsonElement({_to_camel(p.name)})' - for p in body_params - ) - out.append( - f" val body = causewayJson.encodeToString(JsonObject(mapOf({entries})))\n" - ) - else: - out.append(" val body: String? = null\n") - - out.append(f' val resp = request("{route.method}", path, query, headers, body)\n') - - # Streaming routes are handled by _kotlin_stream_method above; we never - # reach this branch through that path. Kept for completeness if a future - # change re-routes a streaming case through the unary builder. - if route.response is None: - out.append(" return\n") - elif route.raises: - success_ty = _kotlin_type(route.response) - enum_name = _kotlin_error_enum_name(route.name) - out.append(" val env = causewayJson.parseToJsonElement(resp).jsonObject\n") - out.append(' val ok = env["ok"]?.jsonPrimitive?.content == "true"\n') - out.append(" if (ok) {\n") - out.append( - f' return causewayJson.decodeFromJsonElement<{success_ty}>(env["data"]!!)\n' - ) - out.append(" }\n") - out.append(' val err = env["error"]!!.jsonObject\n') - out.append(' val kind = err["kind"]!!.jsonPrimitive.content\n') - out.append(" when (kind) {\n") - for v in route.raises: - out.append( - f' "{v.name}" -> throw {enum_name}.Is{_kotlin_ident(v.name)}(' - f"causewayJson.decodeFromJsonElement(err))\n" - ) - out.append(" else -> throw CausewayRPCError(500, resp)\n") - out.append(" }\n") - else: - success_ty = _kotlin_type(route.response) - out.append(f" return causewayJson.decodeFromString<{success_ty}>(resp)\n") - - out.append(" }\n\n") - return "".join(out) - - -def _kotlin_response_type(route: RouteIR) -> str: - if route.streams: - event = _kotlin_type(route.event_schema) if route.event_schema else "String" - return f"kotlinx.coroutines.flow.Flow<{event}>" - if route.response is None: - return "Unit" - return _kotlin_type(route.response) - - -def _kotlin_stream_method(route: RouteIR, method: str, arg_list: str) -> str: - """Emit a streaming endpoint as a Flow backed by HttpURLConnection + line reader.""" - event = _kotlin_type(route.event_schema) if route.event_schema else "String" - out: list[str] = [] - out.append(f" fun {method}({arg_list}): kotlinx.coroutines.flow.Flow<{event}> =\n") - out.append(" kotlinx.coroutines.flow.flow {\n") - out.append(f' var path = "{route.path}"\n') - for p in route.params: - if p.location == "path": - out.append( - f' path = path.replace("{{{p.alias}}}", ' - f"{_to_camel(p.name)}.toString())\n" - ) - queries = [p for p in route.params if p.location == "query"] - if queries: - out.append(" val qs = listOf(\n") - for p in queries: - out.append(f' "{p.alias}" to {_to_camel(p.name)}.toString(),\n') - out.append(' ).joinToString("&") { (k, v) ->\n') - out.append( - ' URLEncoder.encode(k, "UTF-8") + "=" + URLEncoder.encode(v, "UTF-8")\n' - ) - out.append(" }\n") - out.append( - ' val url = URL(baseUrl.trimEnd(\'/\') + path + (if (qs.isEmpty()) "" else "?$qs"))\n' - ) - else: - out.append(" val url = URL(baseUrl.trimEnd('/') + path)\n") - - out.append(" val conn = (url.openConnection() as HttpURLConnection).apply {\n") - out.append(f' requestMethod = "{route.method}"\n') - out.append(' setRequestProperty("Accept", "text/event-stream")\n') - out.append(" for ((k, v) in defaultHeaders) setRequestProperty(k, v)\n") - for p in route.params: - if p.location == "header": - out.append( - f' setRequestProperty("{p.alias}", {_to_camel(p.name)}.toString())\n' - ) - out.append(" }\n") - out.append(" val status = conn.responseCode\n") - out.append(" if (status !in 200..299) {\n") - out.append( - ' val errBody = conn.errorStream?.bufferedReader()?.use { it.readText() } ?: ""\n' - ) - out.append(" throw CausewayRPCError(status, errBody)\n") - out.append(" }\n") - out.append(" conn.inputStream.bufferedReader().use { reader ->\n") - out.append(" val dataBuf = StringBuilder()\n") - out.append(" var eventName: String? = null\n") - out.append(" for (line in reader.lineSequence()) {\n") - out.append(" if (line.isEmpty()) {\n") - out.append(" if (dataBuf.isNotEmpty()) {\n") - out.append(" when (eventName) {\n") - out.append(' "done" -> return@flow\n') - out.append( - ' "error" -> throw CausewayRPCError(status, dataBuf.toString())\n' - ) - out.append(" else -> emit(\n") - out.append( - f" causewayJson.decodeFromString<{event}>(dataBuf.toString())\n" - ) - out.append(" )\n") - out.append(" }\n") - out.append(" }\n") - out.append(" dataBuf.clear()\n") - out.append(" eventName = null\n") - out.append(' } else if (line.startsWith("event: ")) {\n') - out.append(' eventName = line.removePrefix("event: ")\n') - out.append(' } else if (line.startsWith("data: ")) {\n') - out.append(" if (dataBuf.isNotEmpty()) dataBuf.append('\\n')\n") - out.append(' dataBuf.append(line.removePrefix("data: "))\n') - out.append(" }\n") - out.append(" }\n") - out.append(" }\n") - out.append(" }.flowOn(kotlinx.coroutines.Dispatchers.IO)\n\n") - return "".join(out) - - -def _kotlin_error_enum_name(method_name: str) -> str: - return _kotlin_ident(_to_camel(method_name)[:1].upper() + _to_camel(method_name)[1:]) + "Error" - - -def _kotlin_type(schema: Any) -> str: - if not isinstance(schema, dict): - return "kotlinx.serialization.json.JsonElement" - s = cast("dict[str, Any]", schema) - ref = _ref_name(s) - if ref is not None: - return _kotlin_ident(ref) - t = s.get("type") - if isinstance(t, list): - non_null_t = [x for x in cast("list[Any]", t) if x != "null"] # type: ignore[redundant-cast] - if non_null_t: - inner = _kotlin_type( - {"type": non_null_t[0], **{k: v for k, v in s.items() if k != "type"}} - ) - return f"{inner}?" - return "kotlinx.serialization.json.JsonElement?" - if t == "string": - return "String" - if t == "integer": - return "Long" - if t == "number": - return "Double" - if t == "boolean": - return "Boolean" - if t == "array": - items = s.get("items") - return f"List<{_kotlin_type(items)}>" - if "anyOf" in s or "oneOf" in s: - variants = cast("list[Any]", s.get("anyOf") or s.get("oneOf")) - non_null_k: list[Any] = [v for v in variants if not _is_null_schema(v)] - if len(non_null_k) == 1: - return f"{_kotlin_type(non_null_k[0])}?" - return "kotlinx.serialization.json.JsonElement" - return "kotlinx.serialization.json.JsonElement" - - -def _kotlin_ident(name: str) -> str: - cleaned = "".join(c if c.isalnum() or c == "_" else "_" for c in name) - return cleaned or "_" diff --git a/packages/causeway/src/causeway/adapters.py b/packages/causeway/src/causeway/adapters.py index 0f5d0d8..9d29725 100644 --- a/packages/causeway/src/causeway/adapters.py +++ b/packages/causeway/src/causeway/adapters.py @@ -1,11 +1,8 @@ from __future__ import annotations -import asyncio import contextlib -import logging import time -from collections import defaultdict -from collections.abc import AsyncIterator, Awaitable, Callable +from collections.abc import AsyncIterator from typing import Any, ClassVar @@ -96,34 +93,6 @@ async def expire(self, key: str, ttl: int) -> None: self._exp[key] = time.monotonic() + ttl -class CookieStore(_Ready): - """In-memory session store keyed by session id.""" - - def __init__(self) -> None: - self._sessions: dict[str, dict[str, Any]] = {} - - async def shutdown(self) -> None: - self._sessions.clear() - - async def read(self, session_id: str) -> dict[str, Any] | None: - return self._sessions.get(session_id) - - async def write(self, session_id: str, data: dict[str, Any]) -> None: - self._sessions[session_id] = dict(data) - - async def destroy(self, session_id: str) -> None: - self._sessions.pop(session_id, None) - - async def rotate(self, session_id: str) -> str: - import secrets - - new_id = secrets.token_urlsafe(32) - data = self._sessions.pop(session_id, None) - if data is not None: - self._sessions[new_id] = data - return new_id - - class MemoryLimiter(_Ready): """Token-bucket limiter.""" @@ -200,36 +169,6 @@ async def timer(self, name: str, **tags: str) -> AsyncIterator[None]: yield -class StdoutLogSink(_Ready): - """Forwards structured records to logging.""" - - def __init__(self) -> None: - self._log = logging.getLogger("causeway.app") - - async def shutdown(self) -> None: ... - - def emit(self, record: dict[str, Any]) -> None: - self._log.info("%s", record) - - -class MemoryBus(_Ready): - """In-process pub/sub.""" - - def __init__(self) -> None: - self._subs: dict[str, list[Callable[[bytes], Awaitable[None]]]] = defaultdict(list) - - async def shutdown(self) -> None: - self._subs.clear() - - async def publish(self, topic: str, payload: bytes) -> None: - handlers = list(self._subs.get(topic, ())) - if handlers: - await asyncio.gather(*(h(payload) for h in handlers), return_exceptions=True) - - async def subscribe(self, topic: str, handler: Callable[[bytes], Awaitable[None]]) -> None: - self._subs[topic].append(handler) - - class NullScanner(_Ready): """No-op blob scanner.""" @@ -242,13 +181,10 @@ async def scan(self, stream: AsyncIterator[bytes]) -> bool: __all__ = [ - "CookieStore", "LocalStorage", - "MemoryBus", "MemoryKV", "MemoryLimiter", "NullScanner", "NullSink", "StaticFlags", - "StdoutLogSink", ] diff --git a/packages/causeway/src/causeway/cli.py b/packages/causeway/src/causeway/cli.py index 5eef4d0..3225987 100644 --- a/packages/causeway/src/causeway/cli.py +++ b/packages/causeway/src/causeway/cli.py @@ -360,51 +360,6 @@ def openapi( console.print(f"[green]wrote[/green] {out} ({len(ir_value.routes)} routes)") -@app.command() -def swift( - module: Annotated[ - str, - typer.Argument(help="``module:attr`` of your ASGI app."), - ] = "app:app", - out: Annotated[ - Path, - typer.Option("--out", "-o", help="Where to write the Swift client."), - ] = Path("Causeway.swift"), -) -> None: - """Generate a Swift client off the same IR.""" - from causeway._runtime.ir import build_ir - from causeway._runtime.polyglot import write_swift - - app_obj = _load_runtime_app(module) - ir_value = build_ir(app_obj) - write_swift(ir_value, out) - console.print(f"[green]wrote[/green] {out} ({len(ir_value.routes)} routes)") - - -@app.command() -def kotlin( - module: Annotated[ - str, - typer.Argument(help="``module:attr`` of your ASGI app."), - ] = "app:app", - out: Annotated[ - Path, - typer.Option("--out", "-o", help="Where to write the Kotlin client."), - ] = Path("Causeway.kt"), - package: Annotated[ - str, typer.Option(help="Kotlin package name for the generated code.") - ] = "com.causeway.generated", -) -> None: - """Generate a Kotlin client off the same IR.""" - from causeway._runtime.ir import build_ir - from causeway._runtime.polyglot import write_kotlin - - app_obj = _load_runtime_app(module) - ir_value = build_ir(app_obj) - write_kotlin(ir_value, out, package=package) - console.print(f"[green]wrote[/green] {out} ({len(ir_value.routes)} routes)") - - @app.command() def diff( baseline: Annotated[Path, typer.Argument(help="Baseline IR snapshot.")], diff --git a/packages/causeway/src/causeway/contracts.py b/packages/causeway/src/causeway/contracts.py index 226a8a6..b2427e6 100644 --- a/packages/causeway/src/causeway/contracts.py +++ b/packages/causeway/src/causeway/contracts.py @@ -9,7 +9,7 @@ from __future__ import annotations -from collections.abc import AsyncIterator, Awaitable, Callable +from collections.abc import AsyncIterator from contextlib import AbstractAsyncContextManager from datetime import datetime from typing import Any, ClassVar, Protocol, runtime_checkable @@ -138,16 +138,6 @@ async def incr(self, key: str, by: int = 1) -> int: ... async def expire(self, key: str, ttl: int) -> None: ... -@runtime_checkable -class SessionStore(Plugin, Protocol): - contract_version: ClassVar[str] = "v1.0" - - async def read(self, session_id: str) -> dict[str, Any] | None: ... - async def write(self, session_id: str, data: dict[str, Any]) -> None: ... - async def destroy(self, session_id: str) -> None: ... - async def rotate(self, session_id: str) -> str: ... - - @runtime_checkable class Mailer(Plugin, Protocol): contract_version: ClassVar[str] = "v1.0" @@ -157,14 +147,6 @@ async def send_template(self, to: str, template: str, data: dict[str, Any]) -> N async def verify_address(self, address: str) -> bool: ... -@runtime_checkable -class PubSub(Plugin, Protocol): - contract_version: ClassVar[str] = "v1.0" - - async def publish(self, topic: str, payload: bytes) -> None: ... - async def subscribe(self, topic: str, handler: Callable[[bytes], Awaitable[None]]) -> None: ... - - @runtime_checkable class RateLimiter(Plugin, Protocol): contract_version: ClassVar[str] = "v1.0" @@ -193,23 +175,6 @@ def histogram(self, name: str, value: float, **tags: str) -> None: ... def timer(self, name: str, **tags: str) -> AsyncContextManager[None]: ... -@runtime_checkable -class LogSink(Plugin, Protocol): - contract_version: ClassVar[str] = "v1.0" - - def emit(self, record: dict[str, Any]) -> None: ... - - -@runtime_checkable -class Searchable(Plugin, Protocol): - contract_version: ClassVar[str] = "v1.0" - - async def index(self, doc_id: str, doc: dict[str, Any]) -> None: ... - async def search(self, query: str, *, limit: int = 20) -> list[dict[str, Any]]: ... - async def delete(self, doc_id: str) -> None: ... - async def bulk_index(self, docs: list[tuple[str, dict[str, Any]]]) -> None: ... - - @runtime_checkable class DBSession(Plugin, Protocol): contract_version: ClassVar[str] = "v1.0" @@ -318,14 +283,10 @@ def subscribers_for(self, wire_name: str) -> AsyncIterator[Any]: ... "DBSession", "DeployTarget", "FeatureFlags", - "LogSink", "Mailer", "MetricsSink", "Plugin", - "PubSub", "RateLimiter", - "Searchable", - "SessionStore", "Storage", "TaskAdapter", "TaskRef", diff --git a/packages/causeway/src/causeway/contrib/fly.py b/packages/causeway/src/causeway/contrib/fly.py deleted file mode 100644 index 97d5b5c..0000000 --- a/packages/causeway/src/causeway/contrib/fly.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Fly.io deploy target — emits ``fly.toml`` + Dockerfile, calls ``flyctl``.""" - -from __future__ import annotations - -import shutil -import subprocess -from pathlib import Path -from typing import Any, ClassVar - -_FLY_TOML = """\ -# Generated by causeway.contrib.fly. -app = "{name}" -primary_region = "iad" - -[build] - dockerfile = "Dockerfile" - -[http_service] - internal_port = 8000 - force_https = true - auto_stop_machines = true - auto_start_machines = true - min_machines_running = 0 - -[[services]] - internal_port = 8000 - protocol = "tcp" -""" - - -class FlyDeploy: - contract_version: ClassVar[str] = "v1.0" - - def __init__(self, *, app_name: str = "causeway-app") -> None: - self.app_name = app_name - - async def startup(self, settings: Any) -> None: ... - async def shutdown(self) -> None: ... - async def ready(self) -> bool: - return True - - def manifest(self) -> dict[str, Any]: - return { - "target": "fly", - "app": self.app_name, - "files": ["fly.toml", "Dockerfile"], - } - - def package(self, *, target_dir: str | Path = ".") -> bytes: - target = Path(target_dir) - target.mkdir(parents=True, exist_ok=True) - body = _FLY_TOML.format(name=self.app_name) - (target / "fly.toml").write_text(body) - try: - from causeway.contrib.docker import DockerDeploy - - DockerDeploy().package(target_dir=target) - except ImportError: - (target / "Dockerfile").write_text( - "FROM python:3.13-slim\nWORKDIR /app\nCOPY . .\n" - 'CMD ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "8000"]\n', - ) - return body.encode() - - async def push(self, target: str) -> str: - del target - if shutil.which("flyctl") is None: - msg = "flyctl not on PATH — install Fly's CLI to push." - raise RuntimeError(msg) - result = subprocess.run( - ["flyctl", "deploy", "--app", self.app_name], - check=False, - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise RuntimeError(f"flyctl deploy failed: {result.stderr}") - return result.stdout - - -def plugin(settings: Any) -> None: - from causeway import register - - name = getattr(settings, "fly_app", None) or "causeway-app" - register(FlyDeploy(app_name=str(name))) - - -__all__ = ["FlyDeploy", "plugin"] diff --git a/packages/causeway/src/causeway/contrib/growthbook.py b/packages/causeway/src/causeway/contrib/growthbook.py deleted file mode 100644 index 6f96ef5..0000000 --- a/packages/causeway/src/causeway/contrib/growthbook.py +++ /dev/null @@ -1,59 +0,0 @@ -"""GrowthBook adapter for :class:`causeway.contracts.FeatureFlags`.""" - -from __future__ import annotations - -from typing import Any, ClassVar - -import httpx -from growthbook import GrowthBook - - -class GrowthBookFlags: - contract_version: ClassVar[str] = "v1.0" - - def __init__(self, *, api_host: str, client_key: str) -> None: - self.api_host = api_host - self.client_key = client_key - self._features: dict[str, Any] = {} - - async def startup(self, settings: Any) -> None: - await self.refresh() - - async def shutdown(self) -> None: - self._features.clear() - - async def ready(self) -> bool: - return bool(self._features) - - async def refresh(self) -> None: - url = f"{self.api_host.rstrip('/')}/api/features/{self.client_key}" - async with httpx.AsyncClient(timeout=5.0) as client: - resp = await client.get(url) - resp.raise_for_status() - self._features = resp.json().get("features", {}) - - def _gb(self, user: str | None) -> GrowthBook: - return GrowthBook( - features=self._features, - attributes={"id": user} if user else {}, - ) - - async def is_on(self, flag: str, user: str | None = None) -> bool: - return bool(self._gb(user).is_on(flag)) - - async def variant(self, flag: str, user: str | None = None) -> str | None: - result = self._gb(user).eval_feature(flag) - return None if result.value is None else str(result.value) - - -def plugin(settings: Any) -> None: - from causeway import register - - host = getattr(settings, "growthbook_api_host", None) - key = getattr(settings, "growthbook_client_key", None) - if not host or not key: - return - register(GrowthBookFlags(api_host=str(host), client_key=str(key))) - - -__all__ = ["GrowthBookFlags", "plugin"] diff --git a/packages/causeway/src/causeway/contrib/modal.py b/packages/causeway/src/causeway/contrib/modal.py deleted file mode 100644 index ac9c6b2..0000000 --- a/packages/causeway/src/causeway/contrib/modal.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Modal deploy target. - -Emits a ``modal_app.py`` that wraps the Causeway app under a Modal ``@asgi_app`` -function. ``modal deploy modal_app.py`` then ships it. -""" - -from __future__ import annotations - -import shutil -import subprocess -from pathlib import Path -from typing import Any, ClassVar - -_MODAL_APP = '''\ -"""Generated by causeway.contrib.modal.""" - -import modal - -image = modal.Image.debian_slim().pip_install("causeway", "uvicorn[standard]") - -app = modal.App("{name}", image=image) - - -@app.function() -@modal.asgi_app() -def fastapi_app(): - from app.app import app as causeway_app - - return causeway_app -''' - - -class ModalDeploy: - contract_version: ClassVar[str] = "v1.0" - - def __init__(self, *, app_name: str = "causeway-app") -> None: - self.app_name = app_name - - async def startup(self, settings: Any) -> None: ... - async def shutdown(self) -> None: ... - async def ready(self) -> bool: - return True - - def manifest(self) -> dict[str, Any]: - return {"target": "modal", "app": self.app_name, "files": ["modal_app.py"]} - - def package(self, *, target_dir: str | Path = ".") -> bytes: - target = Path(target_dir) - target.mkdir(parents=True, exist_ok=True) - body = _MODAL_APP.format(name=self.app_name) - (target / "modal_app.py").write_text(body) - return body.encode() - - async def push(self, target: str) -> str: - del target - if shutil.which("modal") is None: - msg = "modal not on PATH — install Modal's CLI to deploy." - raise RuntimeError(msg) - result = subprocess.run( - ["modal", "deploy", "modal_app.py"], - check=False, - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise RuntimeError(f"modal deploy failed: {result.stderr}") - return result.stdout - - -def plugin(settings: Any) -> None: - from causeway import register - - name = getattr(settings, "modal_app", None) or "causeway-app" - register(ModalDeploy(app_name=str(name))) - - -__all__ = ["ModalDeploy", "plugin"] diff --git a/packages/causeway/src/causeway/contrib/sentry.py b/packages/causeway/src/causeway/contrib/sentry.py deleted file mode 100644 index 6910524..0000000 --- a/packages/causeway/src/causeway/contrib/sentry.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Sentry integration. Initializes the SDK on startup, lets the SDK's -ASGI/Starlette integrations handle the rest. -""" - -from __future__ import annotations - -from typing import Any, ClassVar - -import sentry_sdk -from sentry_sdk.integrations.asyncio import AsyncioIntegration -from sentry_sdk.integrations.starlette import StarletteIntegration - - -class SentryObserver: - contract_version: ClassVar[str] = "v1.0" - - def __init__( - self, - *, - dsn: str, - environment: str = "dev", - traces_sample_rate: float = 0.1, - ) -> None: - self.dsn = dsn - self.environment = environment - self.traces_sample_rate = traces_sample_rate - - async def startup(self, settings: Any) -> None: - sentry_sdk.init( - dsn=self.dsn, - environment=self.environment, - traces_sample_rate=self.traces_sample_rate, - integrations=[StarletteIntegration(), AsyncioIntegration()], - ) - - async def shutdown(self) -> None: - client = sentry_sdk.get_client() - if client is not None: - client.close() - - async def ready(self) -> bool: - return sentry_sdk.get_client() is not None - - -def plugin(settings: Any) -> None: - from causeway import env, register - - dsn = getattr(settings, "sentry_dsn", None) - if not dsn: - return - if hasattr(dsn, "get_secret_value"): - dsn = dsn.get_secret_value() - register(SentryObserver(dsn=str(dsn), environment=env())) - - -__all__ = ["SentryObserver", "plugin"] diff --git a/packages/causeway/src/causeway/contrib/smtp.py b/packages/causeway/src/causeway/contrib/smtp.py deleted file mode 100644 index 40d77e9..0000000 --- a/packages/causeway/src/causeway/contrib/smtp.py +++ /dev/null @@ -1,77 +0,0 @@ -"""SMTP mailer for :class:`causeway.contracts.Mailer`.""" - -from __future__ import annotations - -from email.message import EmailMessage -from typing import Any, ClassVar - -import aiosmtplib - - -class SmtpMailer: - contract_version: ClassVar[str] = "v1.0" - - def __init__( - self, - *, - host: str, - port: int = 587, - username: str | None = None, - password: str | None = None, - starttls: bool = True, - sender: str = "noreply@localhost", - ) -> None: - self.host = host - self.port = port - self.username = username - self.password = password - self.starttls = starttls - self.sender = sender - - async def startup(self, settings: Any) -> None: ... - async def shutdown(self) -> None: ... - async def ready(self) -> bool: - return True - - async def send(self, to: str, subject: str, body: str) -> None: - msg = EmailMessage() - msg["From"] = self.sender - msg["To"] = to - msg["Subject"] = subject - msg.set_content(body) - await aiosmtplib.send( - msg, - hostname=self.host, - port=self.port, - username=self.username, - password=self.password, - start_tls=self.starttls, - ) - - async def send_template(self, to: str, template: str, data: dict[str, Any]) -> None: - # Renders via ``str.format`` to keep this adapter dependency-free — - # apps that need Jinja / mjml swap in their own MailerContract. - await self.send(to=to, subject=template, body=template.format(**data)) - - async def verify_address(self, address: str) -> bool: - return "@" in address - - -def plugin(settings: Any) -> None: - from causeway import register - - host = getattr(settings, "smtp_host", None) - if not host: - return - register( - SmtpMailer( - host=str(host), - port=int(getattr(settings, "smtp_port", 587)), - username=getattr(settings, "smtp_user", None), - password=getattr(settings, "smtp_password", None), - sender=str(getattr(settings, "smtp_sender", "noreply@localhost")), - ), - ) - - -__all__ = ["SmtpMailer", "plugin"] diff --git a/packages/causeway/src/causeway/plugins.py b/packages/causeway/src/causeway/plugins.py index 4083a7d..c0c6eb6 100644 --- a/packages/causeway/src/causeway/plugins.py +++ b/packages/causeway/src/causeway/plugins.py @@ -102,7 +102,7 @@ def env() -> str: Plugins gate themselves on this for per-env activation:: if env() == "prod": - register(SentryObserver(dsn=...)) + register(S3Storage(bucket=...)) """ return os.environ.get("CAUSEWAY_ENV") or os.environ.get("ENV") or "dev" diff --git a/packages/causeway/tests/contrib/test_deploy_fly.py b/packages/causeway/tests/contrib/test_deploy_fly.py deleted file mode 100644 index 7903d62..0000000 --- a/packages/causeway/tests/contrib/test_deploy_fly.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import annotations - -import subprocess -import types -from pathlib import Path -from typing import Any - -import pytest - -import causeway.plugins as plugin_registry -from causeway.contrib.fly import FlyDeploy, plugin - - -@pytest.fixture(autouse=True) -def _clear_registry() -> None: - plugin_registry.clear() - - -async def test_lifecycle_is_a_no_op() -> None: - target = FlyDeploy(app_name="x") - await target.startup(None) - await target.shutdown() - assert await target.ready() is True - - -def test_manifest_carries_app_name() -> None: - manifest = FlyDeploy(app_name="my-app").manifest() - assert manifest["target"] == "fly" - assert manifest["app"] == "my-app" - assert "fly.toml" in manifest["files"] - assert "Dockerfile" in manifest["files"] - - -def test_package_emits_fly_toml_with_app_name(tmp_path: Path) -> None: - target = FlyDeploy(app_name="hello-fly") - body = target.package(target_dir=tmp_path) - - fly_toml = (tmp_path / "fly.toml").read_text() - assert 'app = "hello-fly"' in fly_toml - assert body == fly_toml.encode() - # Either the docker package wrote the multi-stage Dockerfile, or the - # inline fallback wrote a simpler one — both should exist. - assert (tmp_path / "Dockerfile").is_file() - - -def test_package_falls_back_when_docker_plugin_missing( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - import builtins - - real_import = builtins.__import__ - - def fake_import(name: str, *args: Any, **kwargs: Any) -> Any: - if name == "causeway.contrib.docker": - raise ImportError("simulated missing dep") - return real_import(name, *args, **kwargs) - - monkeypatch.setattr(builtins, "__import__", fake_import) - - FlyDeploy(app_name="solo").package(target_dir=tmp_path) - dockerfile = (tmp_path / "Dockerfile").read_text() - assert "FROM python:3.13-slim" in dockerfile - assert "uvicorn" in dockerfile - - -async def test_push_errors_when_flyctl_missing(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("causeway.contrib.fly.shutil.which", lambda _: None) - with pytest.raises(RuntimeError, match="flyctl not on PATH"): - await FlyDeploy().push("target") - - -async def test_push_invokes_flyctl_and_returns_stdout( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr("causeway.contrib.fly.shutil.which", lambda _: "/usr/bin/flyctl") - - captured: dict[str, Any] = {} - - def fake_run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: - captured["cmd"] = cmd - return subprocess.CompletedProcess(cmd, 0, stdout="deployed\n", stderr="") - - monkeypatch.setattr("causeway.contrib.fly.subprocess.run", fake_run) - out = await FlyDeploy(app_name="x").push("ignored") - assert out == "deployed\n" - assert captured["cmd"] == ["flyctl", "deploy", "--app", "x"] - - -async def test_push_raises_on_flyctl_failure(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("causeway.contrib.fly.shutil.which", lambda _: "/bin/flyctl") - monkeypatch.setattr( - "causeway.contrib.fly.subprocess.run", - lambda cmd, **kw: subprocess.CompletedProcess(cmd, 1, stdout="", stderr="boom"), - ) - with pytest.raises(RuntimeError, match="flyctl deploy failed: boom"): - await FlyDeploy().push("target") - - -def test_plugin_reads_settings_fly_app() -> None: - plugin(types.SimpleNamespace(fly_app="settings-app")) - [adapter] = plugin_registry.registered() - assert isinstance(adapter, FlyDeploy) - assert adapter.app_name == "settings-app" - - -def test_plugin_defaults_when_no_settings() -> None: - plugin(types.SimpleNamespace()) - [adapter] = plugin_registry.registered() - assert adapter.app_name == "causeway-app" diff --git a/packages/causeway/tests/contrib/test_deploy_modal.py b/packages/causeway/tests/contrib/test_deploy_modal.py deleted file mode 100644 index 5ec042f..0000000 --- a/packages/causeway/tests/contrib/test_deploy_modal.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -import subprocess -import types -from pathlib import Path -from typing import Any - -import pytest - -import causeway.plugins as plugin_registry -from causeway.contrib.modal import ModalDeploy, plugin - - -@pytest.fixture(autouse=True) -def _clear_registry() -> None: - plugin_registry.clear() - - -async def test_lifecycle_is_a_no_op() -> None: - target = ModalDeploy(app_name="x") - await target.startup(None) - await target.shutdown() - assert await target.ready() is True - - -def test_manifest_carries_app_name() -> None: - manifest = ModalDeploy(app_name="my-app").manifest() - assert manifest == { - "target": "modal", - "app": "my-app", - "files": ["modal_app.py"], - } - - -def test_package_emits_modal_app(tmp_path: Path) -> None: - target = ModalDeploy(app_name="ship-it") - body = target.package(target_dir=tmp_path) - written = (tmp_path / "modal_app.py").read_text() - assert 'modal.App("ship-it"' in written - assert "from app.app import app as causeway_app" in written - assert body == written.encode() - - -async def test_push_errors_when_modal_missing(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("causeway.contrib.modal.shutil.which", lambda _: None) - with pytest.raises(RuntimeError, match="modal not on PATH"): - await ModalDeploy().push("target") - - -async def test_push_invokes_modal_cli(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("causeway.contrib.modal.shutil.which", lambda _: "/bin/modal") - captured: dict[str, Any] = {} - - def fake_run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: - captured["cmd"] = cmd - return subprocess.CompletedProcess(cmd, 0, stdout="ok\n", stderr="") - - monkeypatch.setattr("causeway.contrib.modal.subprocess.run", fake_run) - out = await ModalDeploy().push("ignored") - assert out == "ok\n" - assert captured["cmd"] == ["modal", "deploy", "modal_app.py"] - - -async def test_push_raises_on_modal_failure(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("causeway.contrib.modal.shutil.which", lambda _: "/bin/modal") - monkeypatch.setattr( - "causeway.contrib.modal.subprocess.run", - lambda cmd, **kw: subprocess.CompletedProcess(cmd, 2, stdout="", stderr="nope"), - ) - with pytest.raises(RuntimeError, match="modal deploy failed: nope"): - await ModalDeploy().push("target") - - -def test_plugin_reads_settings_modal_app() -> None: - plugin(types.SimpleNamespace(modal_app="from-settings")) - [adapter] = plugin_registry.registered() - assert isinstance(adapter, ModalDeploy) - assert adapter.app_name == "from-settings" - - -def test_plugin_defaults_when_no_settings() -> None: - plugin(types.SimpleNamespace()) - [adapter] = plugin_registry.registered() - assert adapter.app_name == "causeway-app" diff --git a/packages/causeway/tests/contrib/test_flags_growthbook.py b/packages/causeway/tests/contrib/test_flags_growthbook.py deleted file mode 100644 index 1eb46e9..0000000 --- a/packages/causeway/tests/contrib/test_flags_growthbook.py +++ /dev/null @@ -1,114 +0,0 @@ -from __future__ import annotations - -import json -import types - -import httpx -import pytest - -import causeway.plugins as plugin_registry -from causeway.contrib.growthbook import GrowthBookFlags, plugin - - -@pytest.fixture(autouse=True) -def _clear_registry() -> None: - plugin_registry.clear() - - -def _features_transport(features: dict[str, dict[str, object]]) -> httpx.MockTransport: - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.path.endswith("/api/features/CK") - return httpx.Response(200, json={"features": features}) - - return httpx.MockTransport(handler) - - -@pytest.fixture -def patch_httpx_client(monkeypatch: pytest.MonkeyPatch): - """Yield a function that swaps ``httpx.AsyncClient`` with a transport-backed double.""" - - def _patch(features: dict[str, dict[str, object]]) -> None: - original = httpx.AsyncClient - - def factory(*args: object, **kwargs: object) -> httpx.AsyncClient: - return original(transport=_features_transport(features)) - - monkeypatch.setattr("causeway.contrib.growthbook.httpx.AsyncClient", factory) - - return _patch - - -async def test_refresh_pulls_features_and_marks_ready(patch_httpx_client) -> None: - patch_httpx_client({"hello": {"defaultValue": True}}) - flags = GrowthBookFlags(api_host="https://gb", client_key="CK") - - assert await flags.ready() is False - await flags.startup(None) - assert await flags.ready() is True - assert await flags.is_on("hello") is True - - -async def test_is_on_falls_back_to_false_for_unknown(patch_httpx_client) -> None: - patch_httpx_client({"hello": {"defaultValue": True}}) - flags = GrowthBookFlags(api_host="https://gb/", client_key="CK") - await flags.startup(None) - assert await flags.is_on("missing") is False - - -async def test_variant_returns_str_or_none(patch_httpx_client) -> None: - patch_httpx_client( - { - "color": {"defaultValue": "blue"}, - "off": {"defaultValue": None}, - } - ) - flags = GrowthBookFlags(api_host="https://gb", client_key="CK") - await flags.startup(None) - assert await flags.variant("color", user="u1") == "blue" - assert await flags.variant("off") is None - - -async def test_shutdown_clears_features(patch_httpx_client) -> None: - patch_httpx_client({"x": {"defaultValue": True}}) - flags = GrowthBookFlags(api_host="https://gb", client_key="CK") - await flags.startup(None) - await flags.shutdown() - assert await flags.ready() is False - - -async def test_refresh_raises_on_http_error(monkeypatch: pytest.MonkeyPatch) -> None: - original = httpx.AsyncClient - - def factory(*args: object, **kwargs: object) -> httpx.AsyncClient: - return original(transport=httpx.MockTransport(lambda req: httpx.Response(500, text="bad"))) - - monkeypatch.setattr("causeway.contrib.growthbook.httpx.AsyncClient", factory) - flags = GrowthBookFlags(api_host="https://gb", client_key="CK") - with pytest.raises(httpx.HTTPStatusError): - await flags.refresh() - - -def test_plugin_no_op_when_settings_missing() -> None: - plugin(types.SimpleNamespace()) - assert plugin_registry.registered() == [] - plugin(types.SimpleNamespace(growthbook_api_host="https://x")) - assert plugin_registry.registered() == [] - - -def test_plugin_registers_when_both_present() -> None: - plugin( - types.SimpleNamespace( - growthbook_api_host="https://x", - growthbook_client_key="CK", - ), - ) - [adapter] = plugin_registry.registered() - assert isinstance(adapter, GrowthBookFlags) - assert adapter.api_host == "https://x" - assert adapter.client_key == "CK" - - -def test_features_serializable() -> None: - """Sanity-check on test fixture shape — keeps the transport handler honest.""" - body = json.dumps({"features": {"x": {"defaultValue": True}}}) - assert "features" in json.loads(body) diff --git a/packages/causeway/tests/contrib/test_mailer_smtp.py b/packages/causeway/tests/contrib/test_mailer_smtp.py deleted file mode 100644 index 0a2f4bb..0000000 --- a/packages/causeway/tests/contrib/test_mailer_smtp.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -import types -from email.message import EmailMessage -from typing import Any - -import pytest - -import causeway.plugins as plugin_registry -from causeway.contrib.smtp import SmtpMailer, plugin - - -@pytest.fixture(autouse=True) -def _clear_registry() -> None: - plugin_registry.clear() - - -@pytest.fixture -def captured_send(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]: - calls: list[dict[str, Any]] = [] - - async def fake_send(msg: EmailMessage, **kwargs: Any) -> tuple[dict[str, Any], str]: - calls.append({"msg": msg, **kwargs}) - return ({}, "ok") - - monkeypatch.setattr("causeway.contrib.smtp.aiosmtplib.send", fake_send) - return calls - - -async def test_lifecycle_methods() -> None: - m = SmtpMailer(host="smtp.example", port=465, starttls=False) - await m.startup(None) - await m.shutdown() - assert await m.ready() is True - - -async def test_send_builds_message_and_dispatches( - captured_send: list[dict[str, Any]], -) -> None: - m = SmtpMailer( - host="smtp.example", - port=2525, - username="u", - password="p", - starttls=False, - sender="from@x", - ) - await m.send(to="to@y", subject="hi", body="payload") - - assert len(captured_send) == 1 - call = captured_send[0] - msg = call["msg"] - assert msg["To"] == "to@y" - assert msg["From"] == "from@x" - assert msg["Subject"] == "hi" - assert msg.get_content().strip() == "payload" - assert call["hostname"] == "smtp.example" - assert call["port"] == 2525 - assert call["username"] == "u" - assert call["password"] == "p" - assert call["start_tls"] is False - - -async def test_send_template_renders_str_format( - captured_send: list[dict[str, Any]], -) -> None: - m = SmtpMailer(host="smtp.example") - await m.send_template( - to="ada@x", - template="Hello {name}, you have {n} messages.", - data={"name": "ada", "n": 3}, - ) - msg = captured_send[0]["msg"] - assert msg.get_content().strip() == "Hello ada, you have 3 messages." - - -async def test_verify_address_basic_check() -> None: - m = SmtpMailer(host="smtp.example") - assert await m.verify_address("a@b") is True - assert await m.verify_address("nope") is False - - -def test_plugin_no_op_without_host() -> None: - plugin(types.SimpleNamespace()) - assert plugin_registry.registered() == [] - - -def test_plugin_reads_settings() -> None: - plugin( - types.SimpleNamespace( - smtp_host="smtp.example", - smtp_port=2525, - smtp_user="u", - smtp_password="p", - smtp_sender="from@x", - ), - ) - [adapter] = plugin_registry.registered() - assert isinstance(adapter, SmtpMailer) - assert adapter.host == "smtp.example" - assert adapter.port == 2525 - assert adapter.sender == "from@x" diff --git a/packages/causeway/tests/contrib/test_observe_sentry.py b/packages/causeway/tests/contrib/test_observe_sentry.py deleted file mode 100644 index 2084299..0000000 --- a/packages/causeway/tests/contrib/test_observe_sentry.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -import types -from typing import Any - -import pytest -from pydantic import SecretStr - -import causeway.plugins as plugin_registry -from causeway.contrib.sentry import SentryObserver, plugin - - -@pytest.fixture(autouse=True) -def _clear_registry() -> None: - plugin_registry.clear() - - -@pytest.fixture -def fake_sentry(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: - state: dict[str, Any] = {"init_kwargs": None, "closed": False, "client": None} - - class _Client: - def close(self) -> None: - state["closed"] = True - - def fake_init(**kwargs: Any) -> None: - state["init_kwargs"] = kwargs - state["client"] = _Client() - - def fake_get_client() -> Any: - return state["client"] - - monkeypatch.setattr("causeway.contrib.sentry.sentry_sdk.init", fake_init) - monkeypatch.setattr("causeway.contrib.sentry.sentry_sdk.get_client", fake_get_client) - return state - - -async def test_startup_calls_sentry_init(fake_sentry: dict[str, Any]) -> None: - obs = SentryObserver(dsn="https://x@sentry/1", environment="staging", traces_sample_rate=0.5) - await obs.startup(None) - - kw = fake_sentry["init_kwargs"] - assert kw is not None - assert kw["dsn"] == "https://x@sentry/1" - assert kw["environment"] == "staging" - assert kw["traces_sample_rate"] == 0.5 - integration_names = [type(i).__name__ for i in kw["integrations"]] - assert "StarletteIntegration" in integration_names - assert "AsyncioIntegration" in integration_names - - -async def test_ready_then_shutdown_closes_client(fake_sentry: dict[str, Any]) -> None: - obs = SentryObserver(dsn="dsn") - await obs.startup(None) - assert await obs.ready() is True - await obs.shutdown() - assert fake_sentry["closed"] is True - - -async def test_ready_false_before_startup(fake_sentry: dict[str, Any]) -> None: - obs = SentryObserver(dsn="dsn") - assert await obs.ready() is False - - -def test_plugin_no_op_without_dsn() -> None: - plugin(types.SimpleNamespace()) - assert plugin_registry.registered() == [] - - -def test_plugin_unwraps_secret_dsn(fake_sentry: dict[str, Any]) -> None: - plugin(types.SimpleNamespace(sentry_dsn=SecretStr("https://k@h/2"))) - [adapter] = plugin_registry.registered() - assert isinstance(adapter, SentryObserver) - assert adapter.dsn == "https://k@h/2" - - -def test_plugin_pulls_env_from_environment( - fake_sentry: dict[str, Any], monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.setenv("CAUSEWAY_ENV", "prod") - plugin(types.SimpleNamespace(sentry_dsn="dsn")) - [adapter] = plugin_registry.registered() - assert isinstance(adapter, SentryObserver) - assert adapter.environment == "prod" diff --git a/packages/causeway/tests/runtime/test_polyglot.py b/packages/causeway/tests/runtime/test_polyglot.py deleted file mode 100644 index de9bc5b..0000000 --- a/packages/causeway/tests/runtime/test_polyglot.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Polyglot codegen tests — Swift + Kotlin renderers emit working clients.""" - -# pyright: basic - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Annotated - -import msgspec - -from causeway._runtime import App, raises, stream -from causeway._runtime.ir import build_ir -from causeway._runtime.params import Header, Query -from causeway._runtime.polyglot import render_kotlin, render_swift - - -def _build_full_ir(): - """A small app exercising path, query, header, body, raises, stream.""" - - class Post(msgspec.Struct): - id: int - title: str - author_name: str # snake_case → camelCase mapping - - class CreatePost(msgspec.Struct): - title: str - body: str - - @dataclass - class PostNotFound(Exception): - post_id: int - - @dataclass - class Forbidden(Exception): - reason: str - - class Tick(msgspec.Struct, tag_field="kind", tag="tick"): - n: int - - app = App() - - @app.get("/posts/{post_id}") - @raises(PostNotFound, Forbidden) - async def get_post( - post_id: int, - authorization: Annotated[str, Header()] = "", - ) -> Post: - del authorization - return Post(id=post_id, title="x", author_name="ada") - - @app.post("/posts") - async def create_post(data: CreatePost) -> Post: - return Post(id=1, title=data.title, author_name="ada") - - @app.get("/search") - async def search(q: Annotated[str, Query()]) -> list[Post]: - return [Post(id=1, title=q, author_name="ada")] - - @app.get("/feed") - async def feed() -> stream[Tick]: - yield Tick(n=1) - - return build_ir(app) - - -# ----------------------- Swift ----------------------- # - - -def test_swift_emits_preamble_and_client() -> None: - out = render_swift(_build_full_ir()) - assert "import Foundation" in out - assert "public struct CausewayClient" in out - assert "convertToSnakeCase" in out # encoder snake_case mapping - assert "convertFromSnakeCase" in out # decoder snake_case mapping - assert "public enum CausewayRPCError" in out - - -def test_swift_struct_with_camel_fields() -> None: - out = render_swift(_build_full_ir()) - assert "public struct Post: Codable" in out - assert "public let id: Int" in out - assert "public let title: String" in out - # author_name → authorName via JSONEncoder/Decoder strategies - assert "public let authorName: String" in out - - -def test_swift_method_with_path_query_header() -> None: - out = render_swift(_build_full_ir()) - assert "func getPost(postId: Int, authorization: String)" in out - assert 'path = path.replacingOccurrences(of: "{post_id}"' in out - # query method - assert "func search(q: String)" in out - assert 'URLQueryItem(name: "q"' in out - # header - assert 'forHTTPHeaderField: "authorization"' in out - - -def test_swift_method_with_body_param() -> None: - out = render_swift(_build_full_ir()) - assert "func createPost(data: CreatePost)" in out - assert "CausewayClient.encoder.encode(data)" in out - - -def test_swift_emits_error_enum_for_raises() -> None: - out = render_swift(_build_full_ir()) - assert "public enum GetPostError: Error {" in out - assert "case postNotFound(PostNotFound)" in out - assert "case forbidden(Forbidden)" in out - - -def test_swift_streaming_method_returns_async_throwing_stream() -> None: - out = render_swift(_build_full_ir()) - # Stream endpoint surfaces an AsyncThrowingStream of the event type. - assert "func feed() async throws -> AsyncThrowingStream" in out - assert "session.bytes(for: req)" in out - assert 'eventName == "done"' in out - assert 'eventName == "error"' in out - assert "CausewayClient.decoder.decode(Tick.self" in out - - -# ----------------------- Kotlin ----------------------- # - - -def test_kotlin_emits_preamble_and_client() -> None: - out = render_kotlin(_build_full_ir(), package="demo.api") - assert "package demo.api" in out - assert "class CausewayClient(" in out - assert "class CausewayRPCError" in out - assert "kotlinx.serialization.Serializable" in out - - -def test_kotlin_data_class_with_serialname() -> None: - out = render_kotlin(_build_full_ir()) - assert "data class Post(" in out - # snake_case → camelCase via @SerialName - assert '@SerialName("author_name") val authorName: String' in out - - -def test_kotlin_method_with_path_query_header() -> None: - out = render_kotlin(_build_full_ir()) - assert "suspend fun getPost(postId: Long, authorization: String):" in out - assert 'path.replace("{post_id}"' in out - assert 'put("authorization"' in out - # query - assert "suspend fun search(q: String):" in out - assert '"q" to q.toString()' in out - - -def test_kotlin_method_with_body_encodes_struct() -> None: - out = render_kotlin(_build_full_ir()) - assert "suspend fun createPost(data: CreatePost):" in out - assert "causewayJson.encodeToString(data)" in out - - -def test_kotlin_emits_sealed_class_for_raises() -> None: - out = render_kotlin(_build_full_ir()) - assert "sealed class GetPostError" in out - assert "Is PostNotFound".replace(" ", "") in out # IsPostNotFound - assert "IsForbidden" in out - - -def test_kotlin_streaming_returns_flow() -> None: - out = render_kotlin(_build_full_ir()) - # Stream endpoint returns a Flow backed by an SSE parser. - assert "fun feed(): kotlinx.coroutines.flow.Flow" in out - assert "flow {" in out - assert "causewayJson.decodeFromString" in out - assert "event: text/event-stream" in out or 'Accept", "text/event-stream' in out - assert "Dispatchers.IO" in out diff --git a/packages/causeway/tests/test_adapters.py b/packages/causeway/tests/test_adapters.py index ae2bd84..0b3adfe 100644 --- a/packages/causeway/tests/test_adapters.py +++ b/packages/causeway/tests/test_adapters.py @@ -2,20 +2,15 @@ from __future__ import annotations -import asyncio - import pytest from causeway.adapters import ( - CookieStore, LocalStorage, - MemoryBus, MemoryKV, MemoryLimiter, NullScanner, NullSink, StaticFlags, - StdoutLogSink, ) # --------------------------------------------------------------------------- @@ -82,20 +77,6 @@ def now() -> float: assert await kv.get("k") is None -# --------------------------------------------------------------------------- -# Sessions -# --------------------------------------------------------------------------- - - -async def test_cookie_store_rotate_moves_data() -> None: - s = CookieStore() - await s.write("old", {"user": "ada"}) - new = await s.rotate("old") - assert new != "old" - assert await s.read("old") is None - assert await s.read(new) == {"user": "ada"} - - # --------------------------------------------------------------------------- # Rate limiter # --------------------------------------------------------------------------- @@ -134,7 +115,7 @@ class _Settings: # --------------------------------------------------------------------------- -# Metrics + logs (no observable side effects, just smoke) +# Metrics (no observable side effects, just smoke) # --------------------------------------------------------------------------- @@ -147,35 +128,6 @@ async def test_null_sink_swallows() -> None: pass -def test_stdout_log_sink_emits() -> None: - StdoutLogSink().emit({"event": "started"}) - - -# --------------------------------------------------------------------------- -# Pub/sub -# --------------------------------------------------------------------------- - - -async def test_pubsub_fans_out_to_every_subscriber() -> None: - bus = MemoryBus() - a_seen: list[bytes] = [] - b_seen: list[bytes] = [] - - async def a(payload: bytes) -> None: - a_seen.append(payload) - - async def b(payload: bytes) -> None: - b_seen.append(payload) - - await bus.subscribe("evt", a) - await bus.subscribe("evt", b) - await bus.publish("evt", b"hi") - # Yield once for tasks fan-out to complete. - await asyncio.sleep(0) - assert a_seen == [b"hi"] - assert b_seen == [b"hi"] - - # --------------------------------------------------------------------------- # Blob scanner # --------------------------------------------------------------------------- diff --git a/packages/causeway/uv.lock b/packages/causeway/uv.lock index ef52336..87deb4c 100644 --- a/packages/causeway/uv.lock +++ b/packages/causeway/uv.lock @@ -201,15 +201,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] -[[package]] -name = "aiosmtplib" -version = "5.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/ba/34f2fef90d13e21ae3f1b360da98d825c40832bb232613513be92457ff65/aiosmtplib-5.1.1.tar.gz", hash = "sha256:d9a35e9d170bc1a9f66e2fdfe7fd212f7eebb8c1581c621f79395d0bcaba7a68", size = 68123, upload-time = "2026-05-31T17:25:36.298Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/97/d1030d897e96c79cf0682ff93c11a2118085b3af4c27993675eda9e55da3/aiosmtplib-5.1.1-py3-none-any.whl", hash = "sha256:9d384f0c3d8906f745c1cf6819f073145bb2de8b10407905f5e2ee3389bfe6c7", size = 27937, upload-time = "2026-05-31T17:25:35.283Z" }, -] - [[package]] name = "aiosqlite" version = "0.22.1" @@ -364,11 +355,7 @@ dependencies = [ [package.optional-dependencies] all = [ { name = "aioboto3" }, - { name = "aiosmtplib" }, { name = "dramatiq", extra = ["redis"] }, - { name = "growthbook" }, - { name = "httpx" }, - { name = "modal" }, { name = "nuitka" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, @@ -377,7 +364,6 @@ all = [ { name = "periodiq" }, { name = "pyjwt" }, { name = "redis" }, - { name = "sentry-sdk" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "sqlmodel" }, ] @@ -388,16 +374,9 @@ dramatiq = [ { name = "dramatiq", extra = ["redis"] }, { name = "periodiq" }, ] -growthbook = [ - { name = "growthbook" }, - { name = "httpx" }, -] jwt = [ { name = "pyjwt" }, ] -modal = [ - { name = "modal" }, -] otel = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, @@ -410,12 +389,6 @@ redis = [ s3 = [ { name = "aioboto3" }, ] -sentry = [ - { name = "sentry-sdk" }, -] -smtp = [ - { name = "aiosmtplib" }, -] sqlmodel = [ { name = "sqlalchemy", extra = ["asyncio"] }, { name = "sqlmodel" }, @@ -437,13 +410,9 @@ dev = [ [package.metadata] requires-dist = [ { name = "aioboto3", marker = "extra == 's3'", specifier = ">=13.0" }, - { name = "aiosmtplib", marker = "extra == 'smtp'", specifier = ">=3.0" }, { name = "anyio", specifier = ">=4.13.0" }, - { name = "causeway", extras = ["jwt", "redis", "sqlmodel", "docker", "fly", "modal", "growthbook", "smtp", "sentry", "fs", "s3", "dramatiq", "otel", "binary"], marker = "extra == 'all'" }, + { name = "causeway", extras = ["jwt", "redis", "sqlmodel", "docker", "fs", "s3", "dramatiq", "otel", "binary"], marker = "extra == 'all'" }, { name = "dramatiq", extras = ["redis"], marker = "extra == 'dramatiq'", specifier = ">=2.1.0" }, - { name = "growthbook", marker = "extra == 'growthbook'", specifier = ">=1.0" }, - { name = "httpx", marker = "extra == 'growthbook'", specifier = ">=0.28" }, - { name = "modal", marker = "extra == 'modal'", specifier = ">=0.65" }, { name = "msgspec", specifier = ">=0.19" }, { name = "nuitka", marker = "extra == 'binary'", specifier = ">=2.5" }, { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.41.1" }, @@ -457,7 +426,6 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.18" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0" }, { name = "rich", specifier = ">=15.0.0" }, - { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.0" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'sqlmodel'", specifier = ">=2.0" }, { name = "sqlmodel", marker = "extra == 'sqlmodel'", specifier = ">=0.0.22" }, { name = "starlette", specifier = ">=1.0.1" }, @@ -466,7 +434,7 @@ requires-dist = [ { name = "uvicorn", extras = ["standard"], specifier = ">=0.47.0" }, { name = "watchfiles", specifier = ">=1.1.1" }, ] -provides-extras = ["jwt", "redis", "sqlmodel", "docker", "fly", "modal", "growthbook", "smtp", "sentry", "fs", "s3", "dramatiq", "otel", "binary", "all"] +provides-extras = ["jwt", "redis", "sqlmodel", "docker", "fs", "s3", "dramatiq", "otel", "binary", "all"] [package.metadata.requires-dev] dev = [ @@ -481,54 +449,6 @@ dev = [ { name = "types-setuptools" }, ] -[[package]] -name = "cbor2" -version = "6.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/af/473c241e41c142ea06ebef8d1f660fa6ff928fb97210e7bec8ee5974f8cd/cbor2-6.1.2.tar.gz", hash = "sha256:6b43037a66947dee5af0abb1a4c3a13b3abac5a4a3f32f9771efbbcd030fd909", size = 86760, upload-time = "2026-06-02T19:01:29.333Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/1e/687d7a712755c84a4b823ca79622dceef7ddfb0a3387b6ac1cad10835e07/cbor2-6.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ce8f6d9e234bdf36b5300bf3da98fafc198b253f8dfe77747327806bdb37d97", size = 411738, upload-time = "2026-06-02T19:00:33.396Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d3/a96162ac244e074f9c188ffd29c086c51466e71c7c360189f6204900db3d/cbor2-6.1.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9f81ab0e74671b0ff9b7e30386e2ab8d40ee1049d13c1680b57ab1b1cd95c81a", size = 457945, upload-time = "2026-06-02T19:00:34.729Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f3/7fed7cee8456932d38e7b11d5034470ee9e91378d16f762c552e78df34fa/cbor2-6.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5a429fc61db768c3b4739eb8532556eed86913ad64fe6ebbc1f3a646fb9a4f22", size = 468758, upload-time = "2026-06-02T19:00:35.882Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e9/bb31f04c5afa53eb55927da1399cc596d7e84e7053de7abf2c3aba0ea3a9/cbor2-6.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:511999bf3310c6641d3d15ee3853daa7ebd6ef3130bb0d63b9a7e2fd720a3714", size = 523169, upload-time = "2026-06-02T19:00:37.422Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7f/90faf18c280abb49428ed2e78f672ef0c7f6eb1b9b685bc4fe810f2e5e95/cbor2-6.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3099e678283efd2d3cabd6ddcb770da6e2102c0d265f98bca38aa4e720e247cf", size = 534885, upload-time = "2026-06-02T19:00:38.972Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8a/447aea5da80847bb17ca4718cd4909a2dc8dfe6f68ede4fe29f94b4ca12c/cbor2-6.1.2-cp311-cp311-win32.whl", hash = "sha256:0ef832ac8152ca76a69c184fe401329629b7dfd5fdddd713121bf1ff6d21660f", size = 284601, upload-time = "2026-06-02T19:00:40.426Z" }, - { url = "https://files.pythonhosted.org/packages/55/95/6239187639a875eb83b924c16f4938d3d735c9c45474008c8b962bd55da2/cbor2-6.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:08cdc03d65e965aafd04c3bf9cb54b8cba55041756bd39d0ba6cd62bd060f959", size = 301284, upload-time = "2026-06-02T19:00:41.693Z" }, - { url = "https://files.pythonhosted.org/packages/76/cb/e5f92271747a0331ca9151fac4098f8e245f1b09623ddff1258967a35b01/cbor2-6.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:0e2cfd25a395d454990d67148103107293c6506c3b0b15952a6e97f53d23deda", size = 292228, upload-time = "2026-06-02T19:00:43.27Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0c/a857b6ca032282b564cf25de18ad92fe0614e8b3fa3422eb10e32a873939/cbor2-6.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:92b158d3ff9d9dce70eeb09786a6e518e3cb0ecb927fd23e9a0f7fc4b175c01a", size = 409592, upload-time = "2026-06-02T19:00:44.556Z" }, - { url = "https://files.pythonhosted.org/packages/29/db/e0518153b3228159d9373f3b5785d7ea2d68898e27ee1bce7d03f0b5f7aa/cbor2-6.1.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d29a11044b07048e19f39a87fe8fea7ea865eb0ace50dc4c29513d52d40e2ddf", size = 454598, upload-time = "2026-06-02T19:00:45.784Z" }, - { url = "https://files.pythonhosted.org/packages/29/67/62127b22edc6011ba55b76a28ab7c2219a45d01871a8199532e0978b26d1/cbor2-6.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a106f174eda34d8937a621c7f3e6044586cb209170cdc8da0ffbea89d1d6e385", size = 467380, upload-time = "2026-06-02T19:00:47.196Z" }, - { url = "https://files.pythonhosted.org/packages/7c/95/7992d8ec904c116ad547abb4960cc3fde695d5853c66596b1465d14d2f7b/cbor2-6.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ea16a25cc457a92879ff7a36cc50b587bddba09d8176bf1a94803eec5aa27eb", size = 521672, upload-time = "2026-06-02T19:00:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/cb/cf/80cc4be132a523f0c92fb4c71813577bb393abea9e27990ca74605e0e930/cbor2-6.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2652a94224980d47f2a3866dd35b1afe532ecdfaf91f8cfcec39a026c457a844", size = 534402, upload-time = "2026-06-02T19:00:50.064Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ea/99e466d8bef61a0775a1d8538ae6c9d95f4533fadc01f8f7814cb7ab80ad/cbor2-6.1.2-cp312-cp312-win32.whl", hash = "sha256:618666292900487db4a5abcade3150105c9c9fdd22576e6ff297c9a72eef0c6a", size = 283225, upload-time = "2026-06-02T19:00:51.406Z" }, - { url = "https://files.pythonhosted.org/packages/14/13/e6a677bdc499e43049006cb54fe605b0f7aef621402d31354cc42ef293c9/cbor2-6.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:c61c0b2e2cee64497e6c62d1976bc212f62ac0cd2b5b903613610d79b8b06b60", size = 300844, upload-time = "2026-06-02T19:00:52.628Z" }, - { url = "https://files.pythonhosted.org/packages/77/4a/08bd8461f8e2e1ce1de5ae2768f2b7ca39a090e3156c1ee0d9b5fd86e70d/cbor2-6.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c871e7266ddc545b258e6f8e5300396985dc485d7ccf8bb4777385782f302153", size = 289040, upload-time = "2026-06-02T19:00:53.971Z" }, - { url = "https://files.pythonhosted.org/packages/2b/dc/bc045c8f36317e4e5f7a60d94b36833139909fc32e3a65f44bc61a36def0/cbor2-6.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f1aa38c422d87ea61849b2a823b10b64053fb4da8763f19ac78ea9a69d682b2a", size = 408846, upload-time = "2026-06-02T19:00:55.476Z" }, - { url = "https://files.pythonhosted.org/packages/2b/36/d66f5f0dd98ecbdcfc7da1fbd423f7b3782a27719f0062a560476f00b334/cbor2-6.1.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ff7d0bd8ff432832338a8d2430aee34f8a082342480ff537c0ba90e2b8ff7894", size = 454624, upload-time = "2026-06-02T19:00:56.744Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/4884b9cf03db14dc5007825d5d1bf8678a75c49d4268d8e0c1c6e9580104/cbor2-6.1.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c1eedf3290d88a5f663bd8b4b8f0f0e2103d0594c293fa5f4e62e53100972309", size = 466585, upload-time = "2026-06-02T19:00:58.209Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/36a15beb3915f56a79d6e9213c6d40c0f5cb90cd3462923f555d78068847/cbor2-6.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3049b04bddf9a5a2d0e5bb25dccdaf4552fcaf607b404e249d4f78f010fcc7d0", size = 521678, upload-time = "2026-06-02T19:00:59.524Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3f/e899313371ebeb7a191d751de97ccd8242abc24bbc9d8e2c58e04475cfb0/cbor2-6.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96eb687a62040401668f06a85de8f47361ef44574de1493899e0ec678109fc04", size = 534044, upload-time = "2026-06-02T19:01:00.875Z" }, - { url = "https://files.pythonhosted.org/packages/1e/5e/1a872acdeb1ab9a884ec3460f73a43e02154dc20d8ccb627bbd60f4c0ea1/cbor2-6.1.2-cp313-cp313-win32.whl", hash = "sha256:03440b505882280023db1fedcee6844804e9968bb50f9eb4ff12aaf27777fcfe", size = 282328, upload-time = "2026-06-02T19:01:02.347Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/29721bc15d38889e7bec214ede2346ee15970bedcc5e6ce1fa30f21e9a4e/cbor2-6.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:d2c8da2c0f821827dcc9eb59a5c9351791a8aa3b389a2ea7ca64c4f97bcb94cf", size = 300313, upload-time = "2026-06-02T19:01:03.69Z" }, - { url = "https://files.pythonhosted.org/packages/07/98/a13b424fb2f14fe332b57f71f479953b2f291a051f797d42ddab9fcd2027/cbor2-6.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:8e1478d3b980ddfcaf56e27cecbfe13057e0f67d5e8240fe8a398815acb9c4bf", size = 288725, upload-time = "2026-06-02T19:01:04.933Z" }, - { url = "https://files.pythonhosted.org/packages/62/72/949bdc7422acd868a2355ae032561a104973fb5de284b36a237b85780dc9/cbor2-6.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b0b65314a0b18c47651e17792447171a858dd77e3f161c451ad850d63f8718a9", size = 407436, upload-time = "2026-06-02T19:01:06.259Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bd/5969f9263102d1c15aa370b39802e4a87b1d1703fdb51588daf38b5fbe7e/cbor2-6.1.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:8904deb2849bae40cea970e114398a19da371e1048ae1409e64f167a1205daf6", size = 453507, upload-time = "2026-06-02T19:01:07.795Z" }, - { url = "https://files.pythonhosted.org/packages/93/a5/227b785692a8374e3dbdf1fe76d1a9af48239855abd68a4111a1458fd81b/cbor2-6.1.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b29d58d8ce00535354d873df170a3e9f0f0a02af65d12102d2552e2129c65dc8", size = 464875, upload-time = "2026-06-02T19:01:09.222Z" }, - { url = "https://files.pythonhosted.org/packages/6d/48/a06527c3fbed4c32816abba4540e432fe9cd7e739a37fef0f205bd0f1e44/cbor2-6.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:27be1cc0abc42f154a48a315c92feb2bfb50397e51c70860460438ea172198a5", size = 519940, upload-time = "2026-06-02T19:01:10.795Z" }, - { url = "https://files.pythonhosted.org/packages/31/1b/0e3f0dac7140d4b94ffbcef765fa4cce0caa1d942060101149de998fa7be/cbor2-6.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8d87fb8a33ff1971cb01511e74b044767cbba1ba536d3dc0b0c48f0d1b62237", size = 532612, upload-time = "2026-06-02T19:01:12.363Z" }, - { url = "https://files.pythonhosted.org/packages/35/2f/5af245e7667b65c6e4a714bb5d89c84de5573b857eba9137533d54bc2e4f/cbor2-6.1.2-cp314-cp314-win32.whl", hash = "sha256:72ba0ea913ca1a8d916867f1b7d414f140982d2873e5d92f8f51de437e08979e", size = 285886, upload-time = "2026-06-02T19:01:13.658Z" }, - { url = "https://files.pythonhosted.org/packages/d9/0a/6303f3e19730450c5a82b97cd2c0ed54855f9108502041305b4c641116cd/cbor2-6.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:c02b7d94fe9914798a346a2f089f0f7f85be71d120d40080916d131fa0bd0442", size = 308808, upload-time = "2026-06-02T19:01:14.944Z" }, - { url = "https://files.pythonhosted.org/packages/cd/61/48f9c5545223dad9d2ea2061a76da739b4047a461297b621fc80ce0f65c0/cbor2-6.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:2af1309865000c401755fd4fdd5550f74ac34c3f79eb7db15f3956714769a5a9", size = 299522, upload-time = "2026-06-02T19:01:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2b/efcc6578b4e6142fb8ec9212c0dee5030345db2092f26aa960236067e717/cbor2-6.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f26e08dd78ee77d103065543a65cfb838948fa8735180ad4d81d939950a1420", size = 402925, upload-time = "2026-06-02T19:01:17.979Z" }, - { url = "https://files.pythonhosted.org/packages/58/f6/58c86aa6246b3e7de473d8ff79ac8cc986e95cafe208899a70d6916012d7/cbor2-6.1.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:596e238f24bf9ede11a1ad08d2115fe78105ed6dda42ce1dd35872e7e91974fd", size = 446201, upload-time = "2026-06-02T19:01:19.481Z" }, - { url = "https://files.pythonhosted.org/packages/c8/12/3b90820583e9860e35cb5e91f3b2cd2ab1bbdf1c57fc63aa572952f5f75f/cbor2-6.1.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:08a62f69fe0f0ee1428d901423853b56bb5c775430f798401f8fac4b9affdecc", size = 460193, upload-time = "2026-06-02T19:01:20.876Z" }, - { url = "https://files.pythonhosted.org/packages/ed/88/c1e841ffb39a8e7163d7d432f7ea0e59b812c5134a449c75b6b8eb8aad08/cbor2-6.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6ca0080e4d8ab0d67c0518ac995d03151a1274b5c295c9e619fb6057c91ae49e", size = 511446, upload-time = "2026-06-02T19:01:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/db/0a/f1ede587a388f127b9fc3d8ecb2f5d948654fed9fc7698f8b05fd90986bf/cbor2-6.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b44eb2f3ea1c8d9cb3e39c345204ec4d9489f8149b78eb5e058b13b14a8c7b07", size = 527683, upload-time = "2026-06-02T19:01:23.639Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/e3210ea45855a8d6173821f712a71a90d23dea0c134c4017c6f666a04fdf/cbor2-6.1.2-cp314-cp314t-win32.whl", hash = "sha256:f93179b4b1ba958b5c37b56969b8f07b4fcf44a83319f47559c59f28a1c564a4", size = 280419, upload-time = "2026-06-02T19:01:25.365Z" }, - { url = "https://files.pythonhosted.org/packages/96/84/b555de26cc01108a72ed1df8eb7ca1d63495a3727045f0f93318dc5f99a8/cbor2-6.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:3c6c3d6598c268abf7068ae75b23b19f708e7a4aa294341b356deb65cb2664f1", size = 302514, upload-time = "2026-06-02T19:01:26.782Z" }, - { url = "https://files.pythonhosted.org/packages/d4/6e/5556939414c0d2bffed7c7a53cf2b32181b55a795944d19835d513a7bc88/cbor2-6.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8c2202fd1906f978bff3f97b21351815753dd9a8fcf4612a5113b6b257089059", size = 290058, upload-time = "2026-06-02T19:01:28.077Z" }, -] - [[package]] name = "certifi" version = "2026.4.22" @@ -538,76 +458,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - [[package]] name = "charset-normalizer" version = "3.4.7" @@ -822,65 +672,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "cryptography" -version = "48.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, -] - [[package]] name = "dramatiq" version = "2.1.0" @@ -1102,34 +893,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, ] -[[package]] -name = "growthbook" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "cryptography" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6b/e1/3bf0093ea5fe4831f0ef0af8934850a8339bd5856e62363e88f12fe125d4/growthbook-2.3.0.tar.gz", hash = "sha256:bd6f55c76ddaee925e42b50a5453c7e8794d0afdf55fe03ad67ebd60c6fe04c0", size = 94591, upload-time = "2026-06-05T06:19:15.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/e7/c34e503abe9726e6b7b28341f85489b0376b7cdb747bdd20621ea61e7ac9/growthbook-2.3.0-py2.py3-none-any.whl", hash = "sha256:c7bcaee326bf83f9632bd4015c67446de094e1e6007a2c4cc899c7329e236a67", size = 57081, upload-time = "2026-06-05T06:19:14.729Z" }, -] - -[[package]] -name = "grpclib" -version = "0.4.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h2" }, - { name = "multidict" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/28/5a2c299ec82a876a252c5919aa895a6f1d1d35c96417c5ce4a4660dc3a80/grpclib-0.4.9.tar.gz", hash = "sha256:cc589c330fa81004c6400a52a566407574498cb5b055fa927013361e21466c46", size = 84798, upload-time = "2025-12-14T22:23:14.349Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/90/b0cbbd9efcc82816c58f31a34963071aa19fb792a212a5d9caf8e0fc3097/grpclib-0.4.9-py3-none-any.whl", hash = "sha256:7762ec1c8ed94dfad597475152dd35cbd11aecaaca2f243e29702435ca24cf0e", size = 77063, upload-time = "2025-12-14T22:23:13.224Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1139,28 +902,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] -[[package]] -name = "h2" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hpack" }, - { name = "hyperframe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, -] - -[[package]] -name = "hpack" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -1225,15 +966,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[[package]] -name = "hyperframe" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, -] - [[package]] name = "idna" version = "3.15" @@ -1367,30 +1099,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "modal" -version = "1.4.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "cbor2" }, - { name = "certifi" }, - { name = "click" }, - { name = "grpclib" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "synchronicity" }, - { name = "toml" }, - { name = "types-certifi" }, - { name = "types-toml" }, - { name = "typing-extensions" }, - { name = "watchfiles" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/7d/4126d0fe879ef3e86002ca821a34cb68a2588ea2e8ccb2bfe421d0f42ffe/modal-1.4.3.tar.gz", hash = "sha256:35b2fc840f759b512e12527afb538e1ea4cc232b84cfbfcef3f5d96d5a66abaa", size = 720488, upload-time = "2026-05-18T22:34:45.842Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/54/400262056c144ceee5edab40efa2541ae8928ae5f244fd9025f3ad26c909/modal-1.4.3-py3-none-any.whl", hash = "sha256:802917181f576458a0cb833322157dab09c4f367326426c5a732661a0c519577", size = 826232, upload-time = "2026-05-18T22:34:43.335Z" }, -] - [[package]] name = "msgspec" version = "0.21.1" @@ -1970,15 +1678,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - [[package]] name = "pydantic" version = "2.13.4" @@ -2333,19 +2032,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, ] -[[package]] -name = "sentry-sdk" -version = "2.61.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -2462,27 +2148,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, ] -[[package]] -name = "synchronicity" -version = "0.12.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/d5/e96e6082790c92480380f28aa53e111844cdac7b0f75846f4772cb535a43/synchronicity-0.12.3.tar.gz", hash = "sha256:0d4228b85eaf2805f23b4615b2039a9d24ea811646e2d9f8d0c033094eb85841", size = 60261, upload-time = "2026-05-28T12:33:50.206Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/ea/531a6ea751cbd989da386144810b1b8f529b0aae8c1a9beda8b40966c9c2/synchronicity-0.12.3-py3-none-any.whl", hash = "sha256:e476818cd14102136f41622c619de548f0000c024485fc18521c8fe908ea7574", size = 40982, upload-time = "2026-05-28T12:33:49.125Z" }, -] - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -2552,15 +2217,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, ] -[[package]] -name = "types-certifi" -version = "2021.10.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095, upload-time = "2022-06-09T15:19:05.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136, upload-time = "2022-06-09T15:19:03.127Z" }, -] - [[package]] name = "types-setuptools" version = "82.0.0.20260508" @@ -2570,15 +2226,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/67/f49414a00fc61a4bc64bd0ff879bb230818b68e62c5cf91fc7c098912aac/types_setuptools-82.0.0.20260508-py3-none-any.whl", hash = "sha256:ba1d863bbd11526d7232bca8d5a4aebe1d38fa1677a550f47a2692b7d5776900", size = 68395, upload-time = "2026-05-08T04:47:47.391Z" }, ] -[[package]] -name = "types-toml" -version = "0.10.8.20260518" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/11/6ece999e91f2ccb848ab4420f3f4816e78ac0541f739e6864affdaaa5737/types_toml-0.10.8.20260518.tar.gz", hash = "sha256:80e10facd24fdeda9d5c672187d72be3ac284843788d67f5aae59e3e016db6fe", size = 9419, upload-time = "2026-05-18T06:02:16.719Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/25/489751806bf5c95e4007f8e17409199c54d31e49ffbea07c5729b1286c8e/types_toml-0.10.8.20260518-py3-none-any.whl", hash = "sha256:0e564ab05f6fde62a315b3b5a9b6624fda569399795d30a37e64705a70459303", size = 9669, upload-time = "2026-05-18T06:02:15.86Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d458c84..2594fd8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,5 @@ packages: - "packages/*" - - "examples/*/frontend" # Allow build scripts for trusted toolchain deps. allowBuilds: