dyadpy 0.1.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. dyadpy-0.1.0a0/.gitignore +107 -0
  2. dyadpy-0.1.0a0/CHANGELOG.md +16 -0
  3. dyadpy-0.1.0a0/PKG-INFO +166 -0
  4. dyadpy-0.1.0a0/README.md +121 -0
  5. dyadpy-0.1.0a0/pyproject.toml +171 -0
  6. dyadpy-0.1.0a0/src/dyadpy/__init__.py +68 -0
  7. dyadpy-0.1.0a0/src/dyadpy/_idents.py +11 -0
  8. dyadpy-0.1.0a0/src/dyadpy/_pydantic.py +64 -0
  9. dyadpy-0.1.0a0/src/dyadpy/app.py +210 -0
  10. dyadpy-0.1.0a0/src/dyadpy/bidi.py +99 -0
  11. dyadpy-0.1.0a0/src/dyadpy/cli.py +332 -0
  12. dyadpy-0.1.0a0/src/dyadpy/codegen.py +651 -0
  13. dyadpy-0.1.0a0/src/dyadpy/context.py +192 -0
  14. dyadpy-0.1.0a0/src/dyadpy/diff.py +304 -0
  15. dyadpy-0.1.0a0/src/dyadpy/errors.py +69 -0
  16. dyadpy-0.1.0a0/src/dyadpy/ir.py +291 -0
  17. dyadpy-0.1.0a0/src/dyadpy/openapi.py +122 -0
  18. dyadpy-0.1.0a0/src/dyadpy/otel.py +65 -0
  19. dyadpy-0.1.0a0/src/dyadpy/params.py +105 -0
  20. dyadpy-0.1.0a0/src/dyadpy/polyglot.py +776 -0
  21. dyadpy-0.1.0a0/src/dyadpy/py.typed +0 -0
  22. dyadpy-0.1.0a0/src/dyadpy/runtime.py +842 -0
  23. dyadpy-0.1.0a0/src/dyadpy/streaming.py +78 -0
  24. dyadpy-0.1.0a0/src/dyadpy/tasks.py +252 -0
  25. dyadpy-0.1.0a0/tests/__init__.py +0 -0
  26. dyadpy-0.1.0a0/tests/test_app.py +63 -0
  27. dyadpy-0.1.0a0/tests/test_bidi.py +79 -0
  28. dyadpy-0.1.0a0/tests/test_cli.py +53 -0
  29. dyadpy-0.1.0a0/tests/test_codegen.py +546 -0
  30. dyadpy-0.1.0a0/tests/test_diff.py +170 -0
  31. dyadpy-0.1.0a0/tests/test_diff_formatters.py +53 -0
  32. dyadpy-0.1.0a0/tests/test_e2e_smoke.py +379 -0
  33. dyadpy-0.1.0a0/tests/test_errors_coverage.py +66 -0
  34. dyadpy-0.1.0a0/tests/test_openapi.py +86 -0
  35. dyadpy-0.1.0a0/tests/test_openapi_coverage.py +66 -0
  36. dyadpy-0.1.0a0/tests/test_otel.py +24 -0
  37. dyadpy-0.1.0a0/tests/test_polyglot.py +170 -0
  38. dyadpy-0.1.0a0/tests/test_primitives.py +288 -0
  39. dyadpy-0.1.0a0/tests/test_pydantic.py +87 -0
  40. dyadpy-0.1.0a0/tests/test_pydantic_deep.py +193 -0
  41. dyadpy-0.1.0a0/tests/test_runtime.py +253 -0
  42. dyadpy-0.1.0a0/tests/test_sse_resume.py +73 -0
  43. dyadpy-0.1.0a0/tests/test_tasks.py +86 -0
  44. dyadpy-0.1.0a0/tests/test_tasks_routes.py +113 -0
  45. dyadpy-0.1.0a0/tests/test_uploads.py +62 -0
  46. dyadpy-0.1.0a0/tests/test_validation_errors.py +107 -0
@@ -0,0 +1,107 @@
1
+ # ---- OS ----
2
+ .DS_Store
3
+ Thumbs.db
4
+ desktop.ini
5
+
6
+ # ---- Editors ----
7
+ .idea/
8
+ *.swp
9
+ *.swo
10
+ *~
11
+ .vscode/*
12
+ !.vscode/settings.json
13
+ !.vscode/extensions.json
14
+ !.vscode/launch.json
15
+
16
+ # ---- Node ----
17
+ node_modules/
18
+ .pnp.*
19
+ .yarn/*
20
+ !.yarn/patches
21
+ !.yarn/plugins
22
+ !.yarn/releases
23
+ !.yarn/sdks
24
+ !.yarn/versions
25
+ .npm/
26
+ .eslintcache
27
+ .oxlintcache
28
+ *.tsbuildinfo
29
+ next-env.d.ts
30
+
31
+ # ---- Build output ----
32
+ dist/
33
+ build/
34
+ out/
35
+ .next/
36
+ .turbo/
37
+ .vercel/
38
+ .cache/
39
+ coverage/
40
+ *.log
41
+ npm-debug.log*
42
+ yarn-debug.log*
43
+ yarn-error.log*
44
+ pnpm-debug.log*
45
+
46
+ # ---- Python ----
47
+ __pycache__/
48
+ *.py[cod]
49
+ *$py.class
50
+ *.so
51
+ .Python
52
+ .python-version-local
53
+ .venv/
54
+ venv/
55
+ env/
56
+ ENV/
57
+ .uv-cache/
58
+ .eggs/
59
+ *.egg-info/
60
+ *.egg
61
+ .pytest_cache/
62
+ .mypy_cache/
63
+ .ruff_cache/
64
+ .coverage
65
+ .coverage.*
66
+ htmlcov/
67
+ .tox/
68
+ .nox/
69
+ .hypothesis/
70
+ *.cover
71
+ *.py,cover
72
+
73
+ # ---- Claude Code harness ----
74
+ .claude/
75
+ !.claude/settings.json
76
+ !.claude/commands/
77
+
78
+ # ---- Dyadpy ----
79
+ .dyadpy/
80
+ # Generated TS clients in examples are gitignored;
81
+ # regenerated on dev. Comment out if you want them committed.
82
+ **/lib/dyadpy/client.ts
83
+
84
+ # ---- Benchmarks ----
85
+ benchmarks/results/
86
+ benchmarks/uv.lock
87
+
88
+ # ---- SvelteKit generated ----
89
+ # Everything is regenerated on ``pnpm dev`` / ``svelte-kit sync``. We commit
90
+ # just the tsconfig.json so editor TypeScript diagnostics don't break on a
91
+ # fresh clone before the first sync has run.
92
+ **/.svelte-kit/*
93
+ !**/.svelte-kit/tsconfig.json
94
+
95
+ # ---- Env / secrets ----
96
+ .env
97
+ .env.*
98
+ !.env.example
99
+ *.pem
100
+ *.key
101
+ *.crt
102
+
103
+ # ---- Misc ----
104
+ *.tgz
105
+ *.zip
106
+ .DS_Store?
107
+ ._*
@@ -0,0 +1,16 @@
1
+ # Changelog · `dyadpy`
2
+
3
+ All notable changes to the `dyadpy` Python package will be documented in this
4
+ file. Managed automatically by [release-please](https://github.com/googleapis/release-please)
5
+ from [Conventional Commits](https://www.conventionalcommits.org/).
6
+
7
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
8
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
+
10
+ ## [Unreleased]
11
+
12
+ ### Added
13
+
14
+ - Initial package scaffold: `App`, route decorators, `Context`,
15
+ `Depends`, `stream`, `@raises`, IR builder, codegen renderer, `dyadpy`
16
+ CLI.
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: dyadpy
3
+ Version: 0.1.0a0
4
+ Summary: A type-safe RPC bridge between Python and TypeScript. The function signature is the contract.
5
+ Project-URL: Homepage, https://github.com/tamimbinhakim/dyadpy
6
+ Project-URL: Documentation, https://github.com/tamimbinhakim/dyadpy/tree/main/docs
7
+ Project-URL: Repository, https://github.com/tamimbinhakim/dyadpy
8
+ Project-URL: Issues, https://github.com/tamimbinhakim/dyadpy/issues
9
+ Project-URL: Changelog, https://github.com/tamimbinhakim/dyadpy/blob/main/packages/dyadpy/CHANGELOG.md
10
+ Author-email: Tamim Bin Hakim <tamimbinhakim@users.noreply.github.com>
11
+ License: MIT
12
+ Keywords: asgi,codegen,fastapi-alternative,msgspec,rpc,sse,streaming,trpc,typescript
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: anyio>=4.6
28
+ Requires-Dist: msgspec>=0.19
29
+ Requires-Dist: python-multipart>=0.0.18
30
+ Requires-Dist: rich>=14.0
31
+ Requires-Dist: starlette>=0.45
32
+ Requires-Dist: typer>=0.15
33
+ Requires-Dist: uvicorn[standard]>=0.32
34
+ Requires-Dist: watchfiles>=1.0
35
+ Provides-Extra: all
36
+ Requires-Dist: opentelemetry-api>=1.27; extra == 'all'
37
+ Requires-Dist: opentelemetry-sdk>=1.27; extra == 'all'
38
+ Requires-Dist: pydantic>=2.10; extra == 'all'
39
+ Provides-Extra: otel
40
+ Requires-Dist: opentelemetry-api>=1.27; extra == 'otel'
41
+ Requires-Dist: opentelemetry-sdk>=1.27; extra == 'otel'
42
+ Provides-Extra: pydantic
43
+ Requires-Dist: pydantic>=2.10; extra == 'pydantic'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # dyadpy (Python)
47
+
48
+ > A type-safe RPC bridge between Python and TypeScript.
49
+
50
+ ```bash
51
+ uv add dyadpy
52
+ ```
53
+
54
+ This is the Python half of [Dyadpy](https://github.com/tamimbinhakim/dyadpy). It
55
+ ships:
56
+
57
+ - A thin ASGI framework (`dyadpy.App`) that uses your function signatures
58
+ as the contract — no separate Pydantic models declared above the
59
+ handler.
60
+ - A type extractor that walks `inspect.signature` +
61
+ `typing.get_type_hints`, normalizes through `msgspec`'s native JSON
62
+ Schema export, and produces a canonical IR.
63
+ - A codegen that turns the IR into a single `client.ts` for your
64
+ frontend.
65
+ - A CLI (`dyadpy dev`, `dyadpy build`, `dyadpy codegen`, `dyadpy init`) that
66
+ runs the whole loop in one process.
67
+
68
+ For the full story, the design rationale, and a side-by-side comparison
69
+ vs. FastAPI + openapi-typescript / tRPC / Encore.ts / Connect-RPC, see
70
+ the [repo README](https://github.com/tamimbinhakim/dyadpy).
71
+
72
+ ## 30-second example
73
+
74
+ ```python
75
+ from dyadpy import App, stream, raises
76
+ from dataclasses import dataclass
77
+ import msgspec
78
+
79
+ app = App()
80
+
81
+ class CreatePost(msgspec.Struct):
82
+ title: str
83
+ body: str
84
+
85
+ class Post(msgspec.Struct):
86
+ id: int
87
+ title: str
88
+ body: str
89
+
90
+ @dataclass
91
+ class PostNotFound(Exception):
92
+ post_id: int
93
+
94
+ @app.post("/posts")
95
+ async def create_post(data: CreatePost) -> Post: ...
96
+
97
+ @app.get("/posts/{post_id}")
98
+ @raises(PostNotFound)
99
+ async def get_post(post_id: int) -> Post: ...
100
+
101
+ class Tick(msgspec.Struct, tag="tick"):
102
+ seq: int
103
+
104
+ @app.get("/ticks")
105
+ async def ticks(count: int) -> stream[Tick]:
106
+ for i in range(count):
107
+ yield Tick(seq=i)
108
+ ```
109
+
110
+ Run it:
111
+
112
+ ```bash
113
+ dyadpy dev server.app:app
114
+ ```
115
+
116
+ The watcher writes `frontend/src/lib/dyadpy/client.ts` automatically. Then
117
+ in your frontend:
118
+
119
+ ```ts
120
+ import { api } from "@/lib/dyadpy/client";
121
+
122
+ const post = await api.createPost({ data: { title: "hi", body: "world" } });
123
+
124
+ for await (const ev of api.ticks({ count: 10 })) {
125
+ /* typed */
126
+ }
127
+ ```
128
+
129
+ ## Primitives in this package
130
+
131
+ | Primitive | Purpose |
132
+ | ------------------------------------------------------------------- | ----------------------------------------------------------- |
133
+ | `App` + `@app.{get,post,put,patch,delete}` | Route decorators. |
134
+ | `Annotated[T, Body / Query / Path / Header / Cookie / File / Form]` | Parameter location markers. |
135
+ | `Annotated[list[T], Query()]` | Repeated query params (`?tag=a&tag=b`). |
136
+ | `Bytes` | Raw request / response bodies. Skips the JSON envelope. |
137
+ | `stream[T]` | Typed SSE — client gets `AsyncIterable<T>`. |
138
+ | `@raises(E1, E2, …)` | Typed error union → `Result<T, E1 \| E2>` on the TS side. |
139
+ | `Context.set_status / set_header / set_cookie / after` | Shape the response without dropping to Starlette. |
140
+ | `Depends(provider)` | DI in the FastAPI shape. |
141
+ | `after(fn, …)` | Run a callback after the response is sent. |
142
+ | `InMemoryBackend` + `TaskBackend` Protocol | Background jobs. |
143
+ | `dyadpy.otel.instrument(app)` | One OpenTelemetry span per request (`dyadpy[otel]`). |
144
+ | `dyadpy openapi / swift / kotlin` (CLI) | Emit OpenAPI 3.1, Swift, or Kotlin clients off the same IR. |
145
+
146
+ Full reference: <https://github.com/tamimbinhakim/dyadpy/blob/main/docs/reference.md>
147
+
148
+ ## Optional extras
149
+
150
+ ```bash
151
+ uv add 'dyadpy[pydantic]' # Pydantic plugin (model_validate + model_json_schema)
152
+ uv add 'dyadpy[otel]' # OpenTelemetry middleware
153
+ uv add 'dyadpy[all]' # everything
154
+ ```
155
+
156
+ ## Scope
157
+
158
+ Dyadpy ships at the wire level: RPC, typed streaming, typed errors,
159
+ cancellation, file uploads, dependency injection. It does **not** ship
160
+ vertical integrations — no LLM types, no React hooks in core, no
161
+ chat-bot primitives. Those layers compose on top of the fundamentals
162
+ and live in their own packages.
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,121 @@
1
+ # dyadpy (Python)
2
+
3
+ > A type-safe RPC bridge between Python and TypeScript.
4
+
5
+ ```bash
6
+ uv add dyadpy
7
+ ```
8
+
9
+ This is the Python half of [Dyadpy](https://github.com/tamimbinhakim/dyadpy). It
10
+ ships:
11
+
12
+ - A thin ASGI framework (`dyadpy.App`) that uses your function signatures
13
+ as the contract — no separate Pydantic models declared above the
14
+ handler.
15
+ - A type extractor that walks `inspect.signature` +
16
+ `typing.get_type_hints`, normalizes through `msgspec`'s native JSON
17
+ Schema export, and produces a canonical IR.
18
+ - A codegen that turns the IR into a single `client.ts` for your
19
+ frontend.
20
+ - A CLI (`dyadpy dev`, `dyadpy build`, `dyadpy codegen`, `dyadpy init`) that
21
+ runs the whole loop in one process.
22
+
23
+ For the full story, the design rationale, and a side-by-side comparison
24
+ vs. FastAPI + openapi-typescript / tRPC / Encore.ts / Connect-RPC, see
25
+ the [repo README](https://github.com/tamimbinhakim/dyadpy).
26
+
27
+ ## 30-second example
28
+
29
+ ```python
30
+ from dyadpy import App, stream, raises
31
+ from dataclasses import dataclass
32
+ import msgspec
33
+
34
+ app = App()
35
+
36
+ class CreatePost(msgspec.Struct):
37
+ title: str
38
+ body: str
39
+
40
+ class Post(msgspec.Struct):
41
+ id: int
42
+ title: str
43
+ body: str
44
+
45
+ @dataclass
46
+ class PostNotFound(Exception):
47
+ post_id: int
48
+
49
+ @app.post("/posts")
50
+ async def create_post(data: CreatePost) -> Post: ...
51
+
52
+ @app.get("/posts/{post_id}")
53
+ @raises(PostNotFound)
54
+ async def get_post(post_id: int) -> Post: ...
55
+
56
+ class Tick(msgspec.Struct, tag="tick"):
57
+ seq: int
58
+
59
+ @app.get("/ticks")
60
+ async def ticks(count: int) -> stream[Tick]:
61
+ for i in range(count):
62
+ yield Tick(seq=i)
63
+ ```
64
+
65
+ Run it:
66
+
67
+ ```bash
68
+ dyadpy dev server.app:app
69
+ ```
70
+
71
+ The watcher writes `frontend/src/lib/dyadpy/client.ts` automatically. Then
72
+ in your frontend:
73
+
74
+ ```ts
75
+ import { api } from "@/lib/dyadpy/client";
76
+
77
+ const post = await api.createPost({ data: { title: "hi", body: "world" } });
78
+
79
+ for await (const ev of api.ticks({ count: 10 })) {
80
+ /* typed */
81
+ }
82
+ ```
83
+
84
+ ## Primitives in this package
85
+
86
+ | Primitive | Purpose |
87
+ | ------------------------------------------------------------------- | ----------------------------------------------------------- |
88
+ | `App` + `@app.{get,post,put,patch,delete}` | Route decorators. |
89
+ | `Annotated[T, Body / Query / Path / Header / Cookie / File / Form]` | Parameter location markers. |
90
+ | `Annotated[list[T], Query()]` | Repeated query params (`?tag=a&tag=b`). |
91
+ | `Bytes` | Raw request / response bodies. Skips the JSON envelope. |
92
+ | `stream[T]` | Typed SSE — client gets `AsyncIterable<T>`. |
93
+ | `@raises(E1, E2, …)` | Typed error union → `Result<T, E1 \| E2>` on the TS side. |
94
+ | `Context.set_status / set_header / set_cookie / after` | Shape the response without dropping to Starlette. |
95
+ | `Depends(provider)` | DI in the FastAPI shape. |
96
+ | `after(fn, …)` | Run a callback after the response is sent. |
97
+ | `InMemoryBackend` + `TaskBackend` Protocol | Background jobs. |
98
+ | `dyadpy.otel.instrument(app)` | One OpenTelemetry span per request (`dyadpy[otel]`). |
99
+ | `dyadpy openapi / swift / kotlin` (CLI) | Emit OpenAPI 3.1, Swift, or Kotlin clients off the same IR. |
100
+
101
+ Full reference: <https://github.com/tamimbinhakim/dyadpy/blob/main/docs/reference.md>
102
+
103
+ ## Optional extras
104
+
105
+ ```bash
106
+ uv add 'dyadpy[pydantic]' # Pydantic plugin (model_validate + model_json_schema)
107
+ uv add 'dyadpy[otel]' # OpenTelemetry middleware
108
+ uv add 'dyadpy[all]' # everything
109
+ ```
110
+
111
+ ## Scope
112
+
113
+ Dyadpy ships at the wire level: RPC, typed streaming, typed errors,
114
+ cancellation, file uploads, dependency injection. It does **not** ship
115
+ vertical integrations — no LLM types, no React hooks in core, no
116
+ chat-bot primitives. Those layers compose on top of the fundamentals
117
+ and live in their own packages.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,171 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dyadpy"
7
+ version = "0.1.0a0"
8
+ description = "A type-safe RPC bridge between Python and TypeScript. The function signature is the contract."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Tamim Bin Hakim", email = "tamimbinhakim@users.noreply.github.com" }]
13
+ keywords = [
14
+ "asgi",
15
+ "rpc",
16
+ "trpc",
17
+ "typescript",
18
+ "codegen",
19
+ "streaming",
20
+ "sse",
21
+ "msgspec",
22
+ "fastapi-alternative",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Framework :: AsyncIO",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3 :: Only",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: Internet :: WWW/HTTP",
36
+ "Topic :: Software Development :: Libraries :: Python Modules",
37
+ "Typing :: Typed",
38
+ ]
39
+
40
+ dependencies = [
41
+ "msgspec>=0.19",
42
+ "starlette>=0.45",
43
+ "uvicorn[standard]>=0.32",
44
+ "watchfiles>=1.0",
45
+ "typer>=0.15",
46
+ "rich>=14.0",
47
+ "anyio>=4.6",
48
+ "python-multipart>=0.0.18",
49
+ ]
50
+
51
+ [project.optional-dependencies]
52
+ pydantic = ["pydantic>=2.10"]
53
+ otel = ["opentelemetry-api>=1.27", "opentelemetry-sdk>=1.27"]
54
+ all = ["dyadpy[pydantic,otel]"]
55
+
56
+ [project.scripts]
57
+ dyadpy = "dyadpy.cli:main"
58
+
59
+ [project.urls]
60
+ Homepage = "https://github.com/tamimbinhakim/dyadpy"
61
+ Documentation = "https://github.com/tamimbinhakim/dyadpy/tree/main/docs"
62
+ Repository = "https://github.com/tamimbinhakim/dyadpy"
63
+ Issues = "https://github.com/tamimbinhakim/dyadpy/issues"
64
+ Changelog = "https://github.com/tamimbinhakim/dyadpy/blob/main/packages/dyadpy/CHANGELOG.md"
65
+
66
+ [dependency-groups]
67
+ dev = [
68
+ "pytest>=9.0",
69
+ "pytest-asyncio>=1.0",
70
+ "pytest-cov>=7.0",
71
+ "httpx>=0.28",
72
+ "ruff>=0.13",
73
+ "mypy>=1.18",
74
+ "types-setuptools",
75
+ "pydantic>=2.10",
76
+ "opentelemetry-api>=1.27",
77
+ "opentelemetry-sdk>=1.27",
78
+ ]
79
+
80
+ [tool.hatch.build.targets.wheel]
81
+ packages = ["src/dyadpy"]
82
+
83
+ [tool.hatch.build.targets.sdist]
84
+ include = ["/src", "/tests", "/README.md", "/CHANGELOG.md"]
85
+
86
+ # ---- ruff ----
87
+ [tool.ruff]
88
+ line-length = 100
89
+ target-version = "py311"
90
+ src = ["src", "tests"]
91
+
92
+ [tool.ruff.lint]
93
+ select = [
94
+ "E",
95
+ "F",
96
+ "W", # pycodestyle / pyflakes
97
+ "I", # isort
98
+ "B", # bugbear
99
+ "UP", # pyupgrade
100
+ "SIM", # simplify
101
+ "C4", # comprehensions
102
+ "PT", # pytest
103
+ "RUF", # ruff
104
+ "TID", # tidy imports
105
+ "PYI", # type stubs
106
+ "ANN", # annotations
107
+ ]
108
+ ignore = [
109
+ "ANN401", # allow Any in select cases
110
+ "E501", # line length handled by formatter
111
+ ]
112
+
113
+ [tool.ruff.lint.per-file-ignores]
114
+ "tests/**" = ["ANN", "B017", "PT011"]
115
+
116
+ # ---- pyright / Pylance ----
117
+ # In tests, ``@app.get(...)`` consumes the decorated function via its side
118
+ # effect (route registration). Pylance can't see that, so it flags every
119
+ # handler as unused. Quiet that *only* for tests; production code keeps strict.
120
+ [tool.pyright]
121
+ include = ["src", "tests"]
122
+ typeCheckingMode = "strict"
123
+
124
+ [[tool.pyright.executionEnvironments]]
125
+ root = "tests"
126
+ reportUnusedFunction = false
127
+
128
+ [tool.ruff.lint.flake8-bugbear]
129
+ # ``Depends(...)`` is the FastAPI-shaped DI marker — used as a default arg by
130
+ # design. Whitelisting it here keeps user code free of B008 noise.
131
+ extend-immutable-calls = ["dyadpy.Depends", "dyadpy.context.Depends"]
132
+
133
+ [tool.ruff.lint.isort]
134
+ known-first-party = ["dyadpy"]
135
+
136
+ [tool.ruff.format]
137
+ quote-style = "double"
138
+ indent-style = "space"
139
+ docstring-code-format = true
140
+
141
+ # ---- mypy ----
142
+ [tool.mypy]
143
+ python_version = "3.11"
144
+ strict = true
145
+ warn_unreachable = true
146
+ warn_redundant_casts = true
147
+ warn_unused_ignores = true
148
+ disallow_any_generics = true
149
+ no_implicit_reexport = true
150
+ files = ["src/dyadpy"]
151
+
152
+ [[tool.mypy.overrides]]
153
+ module = "tests.*"
154
+ disallow_untyped_defs = false
155
+
156
+ # ---- pytest ----
157
+ [tool.pytest.ini_options]
158
+ minversion = "8.0"
159
+ testpaths = ["tests"]
160
+ asyncio_mode = "auto"
161
+ addopts = ["-ra", "--strict-markers", "--strict-config"]
162
+ filterwarnings = ["error"]
163
+
164
+ # ---- coverage ----
165
+ [tool.coverage.run]
166
+ branch = true
167
+ source = ["dyadpy"]
168
+ omit = ["src/dyadpy/cli.py"]
169
+
170
+ [tool.coverage.report]
171
+ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", "\\.\\.\\."]
@@ -0,0 +1,68 @@
1
+ """Dyadpy — a type-safe RPC bridge between Python and TypeScript.
2
+
3
+ The function signature is the contract. See
4
+ https://github.com/tamimbinhakim/dyadpy for full docs.
5
+
6
+ ``dyadpy.tasks`` is loaded lazily via PEP 562 — it drags ``asyncio``'s
7
+ unix-event-loop internals that only matter when you actually queue a
8
+ background job. Bidi is cheap to import eagerly (only ``msgspec`` at
9
+ runtime; ``starlette.websockets`` is ``TYPE_CHECKING``-only) so it stays
10
+ on the default path.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from dyadpy.app import App
18
+ from dyadpy.bidi import BidiChannel, bidi
19
+ from dyadpy.context import Context, Depends, after
20
+ from dyadpy.errors import raises
21
+ from dyadpy.params import Form
22
+ from dyadpy.streaming import SsePayload, stream
23
+
24
+ if TYPE_CHECKING: # pragma: no cover - re-export shape only
25
+ from dyadpy.tasks import (
26
+ InMemoryBackend,
27
+ TaskBackend,
28
+ TaskState,
29
+ mount_task_routes,
30
+ )
31
+
32
+ # Raw-body sentinel: annotate a handler param or return with ``Bytes`` to
33
+ # skip the JSON envelope entirely. Identical to the ``bytes`` builtin; the
34
+ # alias is exported for documentation and explicit-intent reasons.
35
+ Bytes = bytes
36
+
37
+ _LAZY_TASKS = {"InMemoryBackend", "TaskBackend", "TaskState", "mount_task_routes"}
38
+
39
+
40
+ def __getattr__(name: str) -> Any:
41
+ # ``importlib.import_module`` (not ``from dyadpy import ...``) so we don't
42
+ # recurse into this very ``__getattr__`` looking up the submodule.
43
+ if name in _LAZY_TASKS:
44
+ import importlib
45
+
46
+ return getattr(importlib.import_module("dyadpy.tasks"), name)
47
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
48
+
49
+
50
+ __all__ = [
51
+ "App",
52
+ "BidiChannel",
53
+ "Bytes",
54
+ "Context",
55
+ "Depends",
56
+ "Form",
57
+ "InMemoryBackend",
58
+ "SsePayload",
59
+ "TaskBackend",
60
+ "TaskState",
61
+ "after",
62
+ "bidi",
63
+ "mount_task_routes",
64
+ "raises",
65
+ "stream",
66
+ ]
67
+
68
+ __version__ = "0.1.0a0"
@@ -0,0 +1,11 @@
1
+ """Identifier transforms shared between codegen and polyglot renderers."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def to_camel(name: str) -> str:
7
+ """``user_id`` → ``userId``. PascalCase / camelCase / mixed pass through untouched."""
8
+ if "_" not in name:
9
+ return name
10
+ head, *rest = name.split("_")
11
+ return head + "".join(p[:1].upper() + p[1:] for p in rest if p)