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.
- dyadpy-0.1.0a0/.gitignore +107 -0
- dyadpy-0.1.0a0/CHANGELOG.md +16 -0
- dyadpy-0.1.0a0/PKG-INFO +166 -0
- dyadpy-0.1.0a0/README.md +121 -0
- dyadpy-0.1.0a0/pyproject.toml +171 -0
- dyadpy-0.1.0a0/src/dyadpy/__init__.py +68 -0
- dyadpy-0.1.0a0/src/dyadpy/_idents.py +11 -0
- dyadpy-0.1.0a0/src/dyadpy/_pydantic.py +64 -0
- dyadpy-0.1.0a0/src/dyadpy/app.py +210 -0
- dyadpy-0.1.0a0/src/dyadpy/bidi.py +99 -0
- dyadpy-0.1.0a0/src/dyadpy/cli.py +332 -0
- dyadpy-0.1.0a0/src/dyadpy/codegen.py +651 -0
- dyadpy-0.1.0a0/src/dyadpy/context.py +192 -0
- dyadpy-0.1.0a0/src/dyadpy/diff.py +304 -0
- dyadpy-0.1.0a0/src/dyadpy/errors.py +69 -0
- dyadpy-0.1.0a0/src/dyadpy/ir.py +291 -0
- dyadpy-0.1.0a0/src/dyadpy/openapi.py +122 -0
- dyadpy-0.1.0a0/src/dyadpy/otel.py +65 -0
- dyadpy-0.1.0a0/src/dyadpy/params.py +105 -0
- dyadpy-0.1.0a0/src/dyadpy/polyglot.py +776 -0
- dyadpy-0.1.0a0/src/dyadpy/py.typed +0 -0
- dyadpy-0.1.0a0/src/dyadpy/runtime.py +842 -0
- dyadpy-0.1.0a0/src/dyadpy/streaming.py +78 -0
- dyadpy-0.1.0a0/src/dyadpy/tasks.py +252 -0
- dyadpy-0.1.0a0/tests/__init__.py +0 -0
- dyadpy-0.1.0a0/tests/test_app.py +63 -0
- dyadpy-0.1.0a0/tests/test_bidi.py +79 -0
- dyadpy-0.1.0a0/tests/test_cli.py +53 -0
- dyadpy-0.1.0a0/tests/test_codegen.py +546 -0
- dyadpy-0.1.0a0/tests/test_diff.py +170 -0
- dyadpy-0.1.0a0/tests/test_diff_formatters.py +53 -0
- dyadpy-0.1.0a0/tests/test_e2e_smoke.py +379 -0
- dyadpy-0.1.0a0/tests/test_errors_coverage.py +66 -0
- dyadpy-0.1.0a0/tests/test_openapi.py +86 -0
- dyadpy-0.1.0a0/tests/test_openapi_coverage.py +66 -0
- dyadpy-0.1.0a0/tests/test_otel.py +24 -0
- dyadpy-0.1.0a0/tests/test_polyglot.py +170 -0
- dyadpy-0.1.0a0/tests/test_primitives.py +288 -0
- dyadpy-0.1.0a0/tests/test_pydantic.py +87 -0
- dyadpy-0.1.0a0/tests/test_pydantic_deep.py +193 -0
- dyadpy-0.1.0a0/tests/test_runtime.py +253 -0
- dyadpy-0.1.0a0/tests/test_sse_resume.py +73 -0
- dyadpy-0.1.0a0/tests/test_tasks.py +86 -0
- dyadpy-0.1.0a0/tests/test_tasks_routes.py +113 -0
- dyadpy-0.1.0a0/tests/test_uploads.py +62 -0
- 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.
|
dyadpy-0.1.0a0/PKG-INFO
ADDED
|
@@ -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
|
dyadpy-0.1.0a0/README.md
ADDED
|
@@ -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)
|