takibiapi 0.1.0__tar.gz → 0.2.0__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 (43) hide show
  1. {takibiapi-0.1.0 → takibiapi-0.2.0}/Cargo.toml +5 -6
  2. {takibiapi-0.1.0 → takibiapi-0.2.0}/PKG-INFO +19 -5
  3. {takibiapi-0.1.0 → takibiapi-0.2.0}/README.md +15 -2
  4. takibiapi-0.2.0/packages/takibiapi/CHANGELOG.md +21 -0
  5. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/README.md +15 -2
  6. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/src/lib.rs +1 -2
  7. takibiapi-0.2.0/packages/takibiapi/tests/typing/routes.py +177 -0
  8. {takibiapi-0.1.0 → takibiapi-0.2.0}/pyproject.toml +5 -4
  9. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/__init__.py +5 -3
  10. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/app.py +6 -2
  11. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/context.py +75 -15
  12. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/route.py +89 -59
  13. takibiapi-0.1.0/pypi/takibiapi/BUILD.bazel +0 -25
  14. takibiapi-0.1.0/pypi/takibiapi/CHANGELOG.md +0 -7
  15. takibiapi-0.1.0/pypi/takibiapi/python/BUILD.bazel +0 -29
  16. takibiapi-0.1.0/pypi/takibiapi/tests/typing/routes.py +0 -97
  17. {takibiapi-0.1.0 → takibiapi-0.2.0}/Cargo.lock +0 -0
  18. {takibiapi-0.1.0 → takibiapi-0.2.0}/LICENSE +0 -0
  19. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/Cargo.toml +0 -0
  20. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/LICENSE +0 -0
  21. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/build.rs +0 -0
  22. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/src/encode.rs +0 -0
  23. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/src/engine.rs +0 -0
  24. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/src/env.rs +0 -0
  25. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/src/json.rs +0 -0
  26. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/src/plan.rs +0 -0
  27. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/src/router.rs +0 -0
  28. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/tests/test_app.py +0 -0
  29. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/tests/test_cookie.py +0 -0
  30. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/tests/test_cors.py +0 -0
  31. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/tests/test_json.py +0 -0
  32. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/tests/test_router.py +0 -0
  33. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/tests/test_schema.py +0 -0
  34. {takibiapi-0.1.0/pypi → takibiapi-0.2.0/packages}/takibiapi/tests/test_typing.py +0 -0
  35. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/_core.pyi +0 -0
  36. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/cookie.py +0 -0
  37. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/cors.py +0 -0
  38. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/openapi.py +0 -0
  39. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/problems.py +0 -0
  40. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/py.typed +0 -0
  41. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/router.py +0 -0
  42. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/schema.py +0 -0
  43. {takibiapi-0.1.0 → takibiapi-0.2.0}/python/takibiapi/schemas.py +0 -0
@@ -1,17 +1,16 @@
1
- # Cargo workspace. The Rust extension crate lives next to its pyproject.toml in pypi/takibiapi,
1
+ # Cargo workspace. The Rust extension crate lives next to its pyproject.toml in packages/takibiapi,
2
2
  # so that directory is both a uv and a Cargo workspace member. The crate has no version of its own:
3
- # maturin takes the package version from pyproject.toml. Bazel reads this workspace and Cargo.lock
4
- # too (MODULE.bazel), so the crates are pinned once. The lint levels are crate attributes in
5
- # pypi/takibiapi/src/lib.rs, where both build systems see them.
3
+ # maturin takes the package version from pyproject.toml. The lint levels are crate attributes in
4
+ # packages/takibiapi/src/lib.rs.
6
5
  [workspace]
7
6
  resolver = "2"
8
- members = ["pypi/takibiapi"]
7
+ members = ["packages/takibiapi"]
9
8
 
10
9
  [workspace.package]
11
10
  edition = "2021"
12
11
  rust-version = "1.83"
13
12
  license = "MIT"
14
- repository = "https://github.com/nakita628/takibi"
13
+ repository = "https://github.com/nakita628/takibiapi"
15
14
 
16
15
  [workspace.dependencies]
17
16
  pyo3 = "0.29"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: takibiapi
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Operating System :: OS Independent
@@ -18,8 +18,9 @@ Author: nakita628
18
18
  License-Expression: MIT
19
19
  Requires-Python: >=3.12
20
20
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
- Project-URL: Issues, https://github.com/nakita628/takibi/issues
22
- Project-URL: Repository, https://github.com/nakita628/takibi
21
+ Project-URL: Funding, https://github.com/sponsors/nakita628
22
+ Project-URL: Issues, https://github.com/nakita628/takibiapi/issues
23
+ Project-URL: Repository, https://github.com/nakita628/takibiapi
23
24
 
24
25
  # TakibiAPI
25
26
 
@@ -72,6 +73,19 @@ curl localhost:8000/takibi # {"message":"takibi🔥"}
72
73
 
73
74
  A route is a class made with `createRoute`, and its handler takes `c: Context[thatRoute]`. `app.openapi(route, handler)` raises `TypeError` for anything else, and the type checker sees the route's types: `c.req.valid(...)` is the declared request type and `c.json(...)` takes only the declared responses.
74
75
 
76
+ The response's status is typed too. `c.json`, `c.text` and `c.body` take only a status the route declares (200 when none is given, so a route without 200 must name its status), and the handler must return what they build (a `TypedResponse`):
77
+
78
+ ```python
79
+ "responses": {201: {...Todo}, 404: {...Problem}}
80
+
81
+ return c.json(Todo(...), 201) # ok
82
+ return c.json(Todo(...), 200) # type error: 200 is not declared
83
+ return c.body(status=204) # type error: nor 204
84
+ return Response(b"...") # type error at app.openapi: not a declared response
85
+ ```
86
+
87
+ A `"default"` response allows any status. The statuses are inferred from the literal keys, so annotate shared responses with theirs: `errors: Mapping[Literal[400, 404], ResponseConfig[Problem]]`, spread as `{**errors, 200: ...}` (a `dict[int, ...]` is a type error). Which body goes with which status is not checked: `c.json(Todo(...), 404)` passes when both are declared.
88
+
75
89
  ## Requests
76
90
 
77
91
  The request parts in `request` (`params`, `query`, `headers`, `cookies`, a JSON `body`) are checked against their **types** before the handler runs and read with `c.req.valid("param" | "query" | "header" | "cookie" | "json")`:
@@ -144,9 +158,9 @@ def getProfileRouteHandler(c: Context[getProfileRoute]):
144
158
 
145
159
  ## Development
146
160
 
147
- From the repository root: `bazel test //pypi/takibiapi:test` runs this package's tests (on both the Rust engine and the pure-Python reference); `uv sync` builds the extension into the virtualenv for the editor. See the root README.
161
+ From the repository root: `uv sync` builds the extension into the virtualenv, and `uv run pytest packages/takibiapi` runs this package's tests (on both the Rust engine and the pure-Python reference). See the root README.
148
162
 
149
163
  ## License
150
164
 
151
- Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/takibiapi/LICENSE) for more information.
165
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibiapi/blob/main/packages/takibiapi/LICENSE) for more information.
152
166
 
@@ -49,6 +49,19 @@ curl localhost:8000/takibi # {"message":"takibi🔥"}
49
49
 
50
50
  A route is a class made with `createRoute`, and its handler takes `c: Context[thatRoute]`. `app.openapi(route, handler)` raises `TypeError` for anything else, and the type checker sees the route's types: `c.req.valid(...)` is the declared request type and `c.json(...)` takes only the declared responses.
51
51
 
52
+ The response's status is typed too. `c.json`, `c.text` and `c.body` take only a status the route declares (200 when none is given, so a route without 200 must name its status), and the handler must return what they build (a `TypedResponse`):
53
+
54
+ ```python
55
+ "responses": {201: {...Todo}, 404: {...Problem}}
56
+
57
+ return c.json(Todo(...), 201) # ok
58
+ return c.json(Todo(...), 200) # type error: 200 is not declared
59
+ return c.body(status=204) # type error: nor 204
60
+ return Response(b"...") # type error at app.openapi: not a declared response
61
+ ```
62
+
63
+ A `"default"` response allows any status. The statuses are inferred from the literal keys, so annotate shared responses with theirs: `errors: Mapping[Literal[400, 404], ResponseConfig[Problem]]`, spread as `{**errors, 200: ...}` (a `dict[int, ...]` is a type error). Which body goes with which status is not checked: `c.json(Todo(...), 404)` passes when both are declared.
64
+
52
65
  ## Requests
53
66
 
54
67
  The request parts in `request` (`params`, `query`, `headers`, `cookies`, a JSON `body`) are checked against their **types** before the handler runs and read with `c.req.valid("param" | "query" | "header" | "cookie" | "json")`:
@@ -121,8 +134,8 @@ def getProfileRouteHandler(c: Context[getProfileRoute]):
121
134
 
122
135
  ## Development
123
136
 
124
- From the repository root: `bazel test //pypi/takibiapi:test` runs this package's tests (on both the Rust engine and the pure-Python reference); `uv sync` builds the extension into the virtualenv for the editor. See the root README.
137
+ From the repository root: `uv sync` builds the extension into the virtualenv, and `uv run pytest packages/takibiapi` runs this package's tests (on both the Rust engine and the pure-Python reference). See the root README.
125
138
 
126
139
  ## License
127
140
 
128
- Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/takibiapi/LICENSE) for more information.
141
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibiapi/blob/main/packages/takibiapi/LICENSE) for more information.
@@ -0,0 +1,21 @@
1
+ # takibiapi
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Typed response statuses: `c.json`, `c.text` and `c.body` take only a status the route declares (200 when none is given; any status when the route declares `"default"`), and a handler registered with `app.openapi` must return what they build, a `TypedResponse`. A `Response` built by hand, or a status the route does not declare, is a type error
8
+ - **Breaking (types only)**: the statuses are inferred from the literal keys of `responses`, so a status passed as an `int` variable, or shared responses typed `dict[int, ResponseConfig[...]]`, are type errors; annotate them with their codes, `Mapping[Literal[400, 404], ResponseConfig[Problem]]`. `Route` takes a sixth type parameter, the statuses. Nothing changes at run time
9
+ - Export `StatusCode` and `TypedResponse`
10
+
11
+ ## 0.1.1
12
+
13
+ ### Patch Changes
14
+
15
+ - Move to the takibiapi repository (https://github.com/nakita628/takibiapi); the project URLs point there
16
+
17
+ ## 0.1.0
18
+
19
+ ### Minor Changes
20
+
21
+ - Initial release
@@ -49,6 +49,19 @@ curl localhost:8000/takibi # {"message":"takibi🔥"}
49
49
 
50
50
  A route is a class made with `createRoute`, and its handler takes `c: Context[thatRoute]`. `app.openapi(route, handler)` raises `TypeError` for anything else, and the type checker sees the route's types: `c.req.valid(...)` is the declared request type and `c.json(...)` takes only the declared responses.
51
51
 
52
+ The response's status is typed too. `c.json`, `c.text` and `c.body` take only a status the route declares (200 when none is given, so a route without 200 must name its status), and the handler must return what they build (a `TypedResponse`):
53
+
54
+ ```python
55
+ "responses": {201: {...Todo}, 404: {...Problem}}
56
+
57
+ return c.json(Todo(...), 201) # ok
58
+ return c.json(Todo(...), 200) # type error: 200 is not declared
59
+ return c.body(status=204) # type error: nor 204
60
+ return Response(b"...") # type error at app.openapi: not a declared response
61
+ ```
62
+
63
+ A `"default"` response allows any status. The statuses are inferred from the literal keys, so annotate shared responses with theirs: `errors: Mapping[Literal[400, 404], ResponseConfig[Problem]]`, spread as `{**errors, 200: ...}` (a `dict[int, ...]` is a type error). Which body goes with which status is not checked: `c.json(Todo(...), 404)` passes when both are declared.
64
+
52
65
  ## Requests
53
66
 
54
67
  The request parts in `request` (`params`, `query`, `headers`, `cookies`, a JSON `body`) are checked against their **types** before the handler runs and read with `c.req.valid("param" | "query" | "header" | "cookie" | "json")`:
@@ -121,8 +134,8 @@ def getProfileRouteHandler(c: Context[getProfileRoute]):
121
134
 
122
135
  ## Development
123
136
 
124
- From the repository root: `bazel test //pypi/takibiapi:test` runs this package's tests (on both the Rust engine and the pure-Python reference); `uv sync` builds the extension into the virtualenv for the editor. See the root README.
137
+ From the repository root: `uv sync` builds the extension into the virtualenv, and `uv run pytest packages/takibiapi` runs this package's tests (on both the Rust engine and the pure-Python reference). See the root README.
125
138
 
126
139
  ## License
127
140
 
128
- Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/takibiapi/LICENSE) for more information.
141
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibiapi/blob/main/packages/takibiapi/LICENSE) for more information.
@@ -2,8 +2,7 @@
2
2
  //! parsing in Rust, with exactly the results of ``takibiapi.schema``'s Python checker and of Python's
3
3
  //! ``json`` and ``urllib.parse`` (input it cannot match is left to them: ``None``).
4
4
 
5
- // Lint levels live here rather than in Cargo.toml so that cargo and Bazel (rules_rust's clippy
6
- // aspect, which does not read Cargo manifests) apply the same set.
5
+ // Lint levels live here rather than in Cargo.toml, next to the code they apply to.
7
6
  #![warn(missing_debug_implementations, unused_qualifications)]
8
7
  #![warn(clippy::all, clippy::pedantic)]
9
8
  #![warn(
@@ -0,0 +1,177 @@
1
+ # pyright: reportUnnecessaryTypeIgnoreComment=true
2
+ """Static types of routes and handlers (checked by pyright; see typing_test.py)."""
3
+
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from typing import Literal, TypedDict, reveal_type
7
+
8
+ from takibiapi import Context, Response, ResponseConfig, TakibiAPI, TypedResponse, createRoute
9
+
10
+
11
+ class TodoParams(TypedDict):
12
+ todoId: int
13
+
14
+
15
+ class Search(TypedDict):
16
+ q: str
17
+
18
+
19
+ @dataclass
20
+ class NewTodo:
21
+ title: str
22
+
23
+
24
+ @dataclass
25
+ class Todo:
26
+ id: int
27
+ title: str
28
+
29
+
30
+ @dataclass
31
+ class Problem:
32
+ detail: str
33
+
34
+
35
+ @dataclass
36
+ class Other:
37
+ x: int
38
+
39
+
40
+ class putTodoRoute(
41
+ createRoute(
42
+ {
43
+ "method": "put",
44
+ "path": "/todos/{todoId}",
45
+ "request": {
46
+ "params": TodoParams,
47
+ "query": Search,
48
+ "body": {"content": {"application/json": {"schema": NewTodo}}},
49
+ },
50
+ "responses": {
51
+ 200: {"description": "ok", "content": {"application/json": {"schema": Todo}}},
52
+ 404: {"description": "missing", "content": {"application/problem+json": {"schema": Problem}}},
53
+ },
54
+ }
55
+ )
56
+ ):
57
+ """Update a todo."""
58
+
59
+
60
+ async def putTodoRouteHandler(c: Context[putTodoRoute]):
61
+ body = c.req.valid("json")
62
+ reveal_type(body, expected_text="NewTodo")
63
+ reveal_type(c.req.valid("param")["todoId"], expected_text="int")
64
+ reveal_type(c.req.valid("query"), expected_text="Search")
65
+ c.req.valid("header") # no headers type: Any
66
+ reveal_type(await c.req.json(), expected_text="Any") # the raw, unchecked body
67
+ reveal_type(c.req.param("todoId"), expected_text="str") # the raw text
68
+ reveal_type(c.req.query("q"), expected_text="str | None")
69
+ reveal_type(c.req.queries("tag"), expected_text="list[str]")
70
+ c.json(Other(x=1)) # pyright: ignore[reportArgumentType] # not a declared response body
71
+ c.json({"id": 1, "title": "x"}) # pyright: ignore[reportArgumentType] # a dict is not a Todo
72
+ reveal_type(c.json(Problem(detail="x"), 404), expected_text="TypedResponse")
73
+ c.json(Problem(detail="x"), 500) # pyright: ignore[reportArgumentType] # 500 is not declared
74
+ c.text("x", 500) # pyright: ignore[reportArgumentType] # nor for text
75
+ c.body(status=204) # pyright: ignore[reportArgumentType] # nor 204 without content
76
+ params = c.req.valid("param")
77
+ return c.json(Todo(id=params["todoId"], title=body.title), 200)
78
+
79
+
80
+ class getOtherRoute(
81
+ createRoute(
82
+ {
83
+ "method": "get",
84
+ "path": "/other",
85
+ "responses": {200: {"description": "ok", "content": {"application/json": {"schema": list[Search]}}}},
86
+ }
87
+ )
88
+ ):
89
+ pass
90
+
91
+
92
+ def getOtherRouteHandler(c: Context[getOtherRoute]):
93
+ c.json([{"q": "x"}])
94
+ c.json([{"name": "x"}]) # pyright: ignore[reportArgumentType] # wrong TypedDict key
95
+ return c.json([])
96
+
97
+
98
+ app = TakibiAPI()
99
+ app.openapi(putTodoRoute, putTodoRouteHandler)
100
+ app.openapi(getOtherRoute, getOtherRouteHandler)
101
+ app.openapi(putTodoRoute, getOtherRouteHandler) # pyright: ignore[reportArgumentType] # another route's handler
102
+
103
+
104
+ # -- statuses ---------------------------------------------------------------------------------- #
105
+
106
+ errors: Mapping[Literal[400, 404], ResponseConfig[Problem]] = {
107
+ 400: {"description": "bad", "content": {"application/json": {"schema": Problem}}},
108
+ 404: {"description": "missing", "content": {"application/json": {"schema": Problem}}},
109
+ }
110
+
111
+
112
+ class createTodoRoute(
113
+ createRoute(
114
+ {
115
+ "method": "post",
116
+ "path": "/todos",
117
+ "responses": {
118
+ **errors,
119
+ 201: {"description": "created", "content": {"application/json": {"schema": Todo}}},
120
+ 204: {"description": "nothing to create"},
121
+ },
122
+ }
123
+ )
124
+ ):
125
+ pass
126
+
127
+
128
+ def createTodoRouteHandler(c: Context[createTodoRoute]):
129
+ if c.req.query("dry"):
130
+ return c.body(status=204)
131
+ if c.req.query("bad"):
132
+ return c.json(Problem(detail="bad"), 400)
133
+ c.json(Todo(id=1, title="x")) # pyright: ignore[reportArgumentType] # 200 (the default) is not declared
134
+ return c.json(Todo(id=1, title="x"), 201)
135
+
136
+
137
+ reveal_type(createTodoRouteHandler, expected_text="(c: Context[createTodoRoute]) -> TypedResponse")
138
+
139
+
140
+ class withDefaultRoute(
141
+ createRoute(
142
+ {
143
+ "method": "get",
144
+ "path": "/default",
145
+ "responses": {
146
+ 200: {"description": "ok", "content": {"application/json": {"schema": Todo}}},
147
+ "default": {"description": "error", "content": {"application/json": {"schema": Problem}}},
148
+ },
149
+ }
150
+ )
151
+ ):
152
+ pass
153
+
154
+
155
+ def withDefaultRouteHandler(c: Context[withDefaultRoute]):
156
+ if c.req.query("x"):
157
+ return c.text("teapot", 418) # "default" covers any status
158
+ return c.json(Problem(detail="x"), 503)
159
+
160
+
161
+ # Keys typed `int` are not statuses (pyright reports it as either rule, depending on what it checks first).
162
+ untyped: dict[int, ResponseConfig[Problem]] = {}
163
+ createRoute({"method": "get", "path": "/u", "responses": {**untyped}}) # pyright: ignore[reportCallIssue, reportArgumentType]
164
+
165
+
166
+ def returnsResponse(c: Context[getOtherRoute]) -> Response:
167
+ return c.json([])
168
+
169
+
170
+ def returnsTyped(c: Context[getOtherRoute]) -> TypedResponse:
171
+ return c.json([])
172
+
173
+
174
+ app.openapi(createTodoRoute, createTodoRouteHandler)
175
+ app.openapi(withDefaultRoute, withDefaultRouteHandler)
176
+ app.openapi(getOtherRoute, returnsTyped)
177
+ app.openapi(getOtherRoute, returnsResponse) # pyright: ignore[reportArgumentType] # a plain Response is not a declared one
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "takibiapi"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "REST APIs whose OpenAPI document, request checks and handler types come from Python's own types"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -23,8 +23,9 @@ classifiers = [
23
23
  dependencies = []
24
24
 
25
25
  [project.urls]
26
- Repository = "https://github.com/nakita628/takibi"
27
- Issues = "https://github.com/nakita628/takibi/issues"
26
+ Repository = "https://github.com/nakita628/takibiapi"
27
+ Issues = "https://github.com/nakita628/takibiapi/issues"
28
+ Funding = "https://github.com/sponsors/nakita628"
28
29
 
29
30
  [build-system]
30
31
  requires = ["maturin>=1.8,<2"]
@@ -33,5 +34,5 @@ build-backend = "maturin"
33
34
  [tool.maturin]
34
35
  module-name = "takibiapi._core"
35
36
  features = ["pyo3/extension-module"]
36
- manifest-path = "pypi/takibiapi/Cargo.toml"
37
+ manifest-path = "packages/takibiapi/Cargo.toml"
37
38
  python-source = "python"
@@ -10,10 +10,10 @@ serves the OpenAPI document. Request parts are checked against their types only
10
10
  """
11
11
 
12
12
  from takibiapi.app import Fallback, FallbackHandler, FallbackMiddleware, FallbackNext, Middleware, Next, TakibiAPI
13
- from takibiapi.context import Context, Request, Response, ResponseValidationError, problem
13
+ from takibiapi.context import Context, Request, Response, ResponseValidationError, TypedResponse, problem
14
14
  from takibiapi.openapi import OpenAPIConfig
15
15
  from takibiapi.problems import ValidationProblem
16
- from takibiapi.route import RequestConfig, ResponseConfig, Route, RouteConfig, createRoute
16
+ from takibiapi.route import RequestConfig, ResponseConfig, Route, RouteConfig, StatusCode, createRoute
17
17
  from takibiapi.schema import ValidationError
18
18
 
19
19
  __all__ = [
@@ -32,10 +32,12 @@ __all__ = [
32
32
  "ResponseValidationError",
33
33
  "Route",
34
34
  "RouteConfig",
35
+ "StatusCode",
35
36
  "TakibiAPI",
37
+ "TypedResponse",
36
38
  "ValidationError",
37
39
  "ValidationProblem",
38
40
  "createRoute",
39
41
  "problem",
40
42
  ]
41
- __version__ = "0.1.0"
43
+ __version__ = "0.2.0"
@@ -14,7 +14,7 @@ import typing
14
14
  from collections.abc import Awaitable, Callable, Mapping
15
15
  from typing import Any, Self, TypeVar, get_args, get_origin
16
16
 
17
- from .context import Context, MalformedJSONError, RawHeaders, Request, Response, problem
17
+ from .context import Context, MalformedJSONError, RawHeaders, Request, Response, TypedResponse, problem
18
18
  from .openapi import OpenAPIConfig, build_document, check_config
19
19
  from .route import AnyRoute, route_class
20
20
  from .router import new_router
@@ -90,11 +90,15 @@ class TakibiAPI:
90
90
  self._documents: dict[int, bytes] = {}
91
91
 
92
92
  # -- routes ------------------------------------------------------------------------------- #
93
- def openapi(self, route: type[_TR], handler: Callable[[Context[_TR]], Response | Awaitable[Response]]) -> Self:
93
+ def openapi(
94
+ self, route: type[_TR], handler: Callable[[Context[_TR]], TypedResponse | Awaitable[TypedResponse]]
95
+ ) -> Self:
94
96
  """Register ``route`` with its handler.
95
97
 
96
98
  The route must be declared as a class (``class getTodoRoute(createRoute({...}))``) and
97
99
  the handler must take it as ``c: Context[getTodoRoute]``; anything else is a ``TypeError``.
100
+ For the type checker, the handler returns what ``c.json`` / ``c.text`` / ``c.body`` build,
101
+ with a status the route declares.
98
102
  The request parts the route declares are checked against their types before the handler
99
103
  runs (422 when they do not match) and read with ``c.req.valid(...)``.
100
104
  """
@@ -24,6 +24,7 @@ _Q = TypeVar("_Q")
24
24
  _J = TypeVar("_J")
25
25
  _H = TypeVar("_H")
26
26
  _B = TypeVar("_B")
27
+ _S = TypeVar("_S")
27
28
 
28
29
  JSON_TYPE = "application/json"
29
30
  PROBLEM_JSON = "application/problem+json"
@@ -52,7 +53,15 @@ class Response:
52
53
  return self.body.decode()
53
54
 
54
55
  def __repr__(self) -> str:
55
- return f"Response({self.status}, {self.body[:60]!r})"
56
+ return f"{type(self).__name__}({self.status}, {self.body[:60]!r})"
57
+
58
+
59
+ class TypedResponse(Response):
60
+ """A response built by ``c.json`` / ``c.text`` / ``c.body``, whose status the type checker has
61
+ matched against the route's responses. A handler registered with ``app.openapi`` must return
62
+ one, so a ``Response`` built by hand is a type error there."""
63
+
64
+ __slots__ = ()
56
65
 
57
66
 
58
67
  def problem(status: int, title: str, detail: str, instance: str, type_: str = "about:blank", **extra: Any) -> Response:
@@ -118,13 +127,13 @@ class Request(Generic[TR]):
118
127
 
119
128
  # The checked part's type comes from the route: Route[params, query, json, headers, ...].
120
129
  @overload
121
- def valid(self: Request[Route[_P, Any, Any, Any, Any]], target: Literal["param"]) -> _P: ...
130
+ def valid(self: Request[Route[_P, Any, Any, Any, Any, Any]], target: Literal["param"]) -> _P: ...
122
131
  @overload
123
- def valid(self: Request[Route[Any, _Q, Any, Any, Any]], target: Literal["query"]) -> _Q: ...
132
+ def valid(self: Request[Route[Any, _Q, Any, Any, Any, Any]], target: Literal["query"]) -> _Q: ...
124
133
  @overload
125
- def valid(self: Request[Route[Any, Any, _J, Any, Any]], target: Literal["json"]) -> _J: ...
134
+ def valid(self: Request[Route[Any, Any, _J, Any, Any, Any]], target: Literal["json"]) -> _J: ...
126
135
  @overload
127
- def valid(self: Request[Route[Any, Any, Any, _H, Any]], target: Literal["header"]) -> _H: ...
136
+ def valid(self: Request[Route[Any, Any, Any, _H, Any, Any]], target: Literal["header"]) -> _H: ...
128
137
  @overload
129
138
  def valid(self, target: Literal["cookie"]) -> Any: ...
130
139
 
@@ -245,15 +254,30 @@ class Context(Generic[TR]):
245
254
  self._vars: dict[str, Any] | None = None
246
255
  self._responses = responses # {status: {media type: encoder}}: checked responses
247
256
 
257
+ # ``value`` is one of the route's response bodies, and ``status`` one of its statuses (any status
258
+ # when the route declares "default"). A status left out is 200, so it must be declared too.
259
+ @overload
248
260
  def json(
249
- self: Context[Route[Any, Any, Any, Any, _B]],
261
+ self: Context[Route[Any, Any, Any, Any, _B, Literal["default"]]],
250
262
  value: _B,
251
263
  status: int = 200,
252
264
  headers: Mapping[str, str] | None = None,
253
- ) -> Response:
254
- """A JSON response. ``value`` is typed by the route's responses (for the type checker).
255
- With takibi's own schemas it is written as it is; schemas that check responses
256
- (``pydantic-takibiapi``) raise :class:`ResponseValidationError` when it does not match."""
265
+ ) -> TypedResponse: ...
266
+ @overload
267
+ def json(
268
+ self: Context[Route[Any, Any, Any, Any, _B, _S]],
269
+ value: _B,
270
+ status: _S = 200,
271
+ headers: Mapping[str, str] | None = None,
272
+ ) -> TypedResponse: ...
273
+
274
+ def json(
275
+ self: Context[Any], value: Any, status: Any = 200, headers: Mapping[str, str] | None = None
276
+ ) -> TypedResponse:
277
+ """A JSON response. ``value`` and ``status`` are typed by the route's responses (for the
278
+ type checker). With takibi's own schemas it is written as it is; schemas that check
279
+ responses (``pydantic-takibiapi``) raise :class:`ResponseValidationError` when it does not
280
+ match."""
257
281
  if self._responses is not None:
258
282
  encoders = self._responses.get(status)
259
283
  if encoders:
@@ -271,11 +295,47 @@ class Context(Generic[TR]):
271
295
  return self._respond(body, status, headers, JSON_TYPE)
272
296
  return self._respond(dumps(value), status, headers, JSON_TYPE)
273
297
 
274
- def text(self, text: str, status: int = 200, headers: Mapping[str, str] | None = None) -> Response:
298
+ @overload
299
+ def text(
300
+ self: Context[Route[Any, Any, Any, Any, Any, Literal["default"]]],
301
+ text: str,
302
+ status: int = 200,
303
+ headers: Mapping[str, str] | None = None,
304
+ ) -> TypedResponse: ...
305
+ @overload
306
+ def text(
307
+ self: Context[Route[Any, Any, Any, Any, Any, _S]],
308
+ text: str,
309
+ status: _S = 200,
310
+ headers: Mapping[str, str] | None = None,
311
+ ) -> TypedResponse: ...
312
+
313
+ def text(
314
+ self: Context[Any], text: str, status: Any = 200, headers: Mapping[str, str] | None = None
315
+ ) -> TypedResponse:
316
+ """A ``text/plain`` response, with a status the route declares."""
275
317
  return self._respond(text.encode(), status, headers, "text/plain; charset=utf-8")
276
318
 
277
- def body(self, data: bytes = b"", status: int = 200, headers: Mapping[str, str] | None = None) -> Response:
278
- """Raw bytes (``c.body(status=204)`` for no content); set the content type in ``headers``."""
319
+ @overload
320
+ def body(
321
+ self: Context[Route[Any, Any, Any, Any, Any, Literal["default"]]],
322
+ data: bytes = b"",
323
+ status: int = 200,
324
+ headers: Mapping[str, str] | None = None,
325
+ ) -> TypedResponse: ...
326
+ @overload
327
+ def body(
328
+ self: Context[Route[Any, Any, Any, Any, Any, _S]],
329
+ data: bytes = b"",
330
+ status: _S = 200,
331
+ headers: Mapping[str, str] | None = None,
332
+ ) -> TypedResponse: ...
333
+
334
+ def body(
335
+ self: Context[Any], data: bytes = b"", status: Any = 200, headers: Mapping[str, str] | None = None
336
+ ) -> TypedResponse:
337
+ """Raw bytes (``c.body(status=204)`` for no content), with a status the route declares; set
338
+ the content type in ``headers``."""
279
339
  return self._respond(data, status, headers, None)
280
340
 
281
341
  def header(self, name: str, value: str | None, *, append: bool = False) -> None:
@@ -308,10 +368,10 @@ class Context(Generic[TR]):
308
368
 
309
369
  def _respond(
310
370
  self, payload: bytes, status: int, headers: Mapping[str, str] | None, content_type: str | None
311
- ) -> Response:
371
+ ) -> TypedResponse:
312
372
  merged: dict[str, HeaderValue] = {} if content_type is None else {"content-type": content_type}
313
373
  if self._headers:
314
374
  merged.update(self._headers)
315
375
  if headers:
316
376
  merged.update({k.lower(): v for k, v in headers.items()})
317
- return Response(payload, status, merged)
377
+ return TypedResponse(payload, status, merged)
@@ -2,9 +2,10 @@
2
2
 
3
3
  The types are written to the OpenAPI document, request parts are checked against them (types
4
4
  only: ``c.req.valid(...)``), and they type the handler: ``createRoute`` returns
5
- ``type[Route[P, Q, J, H, R]]`` (path params, query, JSON body, headers, response bodies), and a
6
- handler for the route takes ``c: Context[thatRoute]``, so ``c.req.valid("param")`` is ``P`` and
7
- ``c.json(...)`` takes ``R``. Responses are not checked at run time.
5
+ ``type[Route[P, Q, J, H, R, S]]`` (path params, query, JSON body, headers, response bodies, response
6
+ statuses), and a handler for the route takes ``c: Context[thatRoute]``, so ``c.req.valid("param")``
7
+ is ``P``, ``c.json(...)`` takes ``R`` with a status in ``S``, and the handler must return what
8
+ ``c.json`` / ``c.text`` / ``c.body`` build. Responses are not checked at run time.
8
9
  """
9
10
 
10
11
  from __future__ import annotations
@@ -25,9 +26,27 @@ H = TypeVar("H")
25
26
  # Response bodies the route declares (a union over its ``responses``) -> ``c.json(...)``. Covariant
26
27
  # (with ReadOnly fields below) so several responses infer ``Todo | Problem``.
27
28
  R = TypeVar("R", covariant=True)
29
+ # Response statuses the route declares: ``Literal[200, 404]`` (with ``"default"`` when declared). The
30
+ # responses' keys are inferred as this union when they are literals of ``StatusCode``; ``Route``
31
+ # takes it contravariantly, so ``c.json(..., status)`` accepts a status only when the route declares
32
+ # it (and any status when the route declares ``"default"``).
33
+ K = TypeVar("K")
34
+ S = TypeVar("S", contravariant=True)
28
35
 
29
36
  Method = Literal["get", "post", "put", "patch", "delete", "head", "options", "trace"]
30
37
 
38
+ # The HTTP status codes of the IANA registry (and 418). A response key outside them, or one typed
39
+ # ``int``, is a type error: annotate shared responses with their codes,
40
+ # ``errors: Mapping[Literal[400, 404], ResponseConfig[Problem]]``.
41
+ StatusCode = Literal[
42
+ 100, 101, 102, 103,
43
+ 200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
44
+ 300, 301, 302, 303, 304, 305, 307, 308,
45
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
46
+ 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
47
+ 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
48
+ ] # fmt: skip
49
+
31
50
  _T_co = TypeVar("_T_co", covariant=True)
32
51
 
33
52
 
@@ -68,18 +87,19 @@ class ResponseMediaType(TypedDict, Generic[R], total=False):
68
87
 
69
88
 
70
89
  class ResponseConfig(TypedDict, Generic[R], total=False):
71
- """One entry of ``responses``. Annotate shared ones with their body type, e.g.
72
- ``errors: dict[int, ResponseConfig[Problem]]``."""
90
+ """One entry of ``responses``. Annotate shared ones with their statuses and body type, e.g.
91
+ ``errors: Mapping[Literal[400, 404], ResponseConfig[Problem]]``, and spread them:
92
+ ``"responses": {**errors, 200: {...}}``."""
73
93
 
74
94
  description: ReadOnly[str]
75
95
  content: ReadOnly[Mapping[str, ResponseMediaType[R]]]
76
96
  headers: ReadOnly[Mapping[str, Any]]
77
97
 
78
98
 
79
- class _OperationConfig(TypedDict, Generic[R]):
99
+ class _OperationConfig(TypedDict, Generic[R, K]):
80
100
  method: Method
81
101
  path: str # "/todos/{todoId}"
82
- responses: Mapping[Any, ResponseConfig[R]] # status code (or "default") -> response
102
+ responses: Mapping[K, ResponseConfig[R]] # status code (or "default") -> response
83
103
  summary: NotRequired[str]
84
104
  description: NotRequired[str]
85
105
  tags: NotRequired[list[str]]
@@ -89,7 +109,7 @@ class _OperationConfig(TypedDict, Generic[R]):
89
109
  hide: NotRequired[bool] # served, but left out of the OpenAPI document
90
110
 
91
111
 
92
- class RouteConfig(_OperationConfig[R], Generic[P, Q, J, H, R]):
112
+ class RouteConfig(_OperationConfig[R, K], Generic[P, Q, J, H, R, K]):
93
113
  request: NotRequired[RequestConfig[P, Q, J, H]]
94
114
 
95
115
 
@@ -103,7 +123,7 @@ class _RequestP(TypedDict, Generic[P]):
103
123
  cookies: NotRequired[type]
104
124
 
105
125
 
106
- class _RouteP(_OperationConfig[R], Generic[P, R]):
126
+ class _RouteP(_OperationConfig[R, K], Generic[P, R, K]):
107
127
  request: _RequestP[P]
108
128
 
109
129
 
@@ -112,7 +132,7 @@ class _RequestQ(TypedDict, Generic[Q]):
112
132
  cookies: NotRequired[type]
113
133
 
114
134
 
115
- class _RouteQ(_OperationConfig[R], Generic[Q, R]):
135
+ class _RouteQ(_OperationConfig[R, K], Generic[Q, R, K]):
116
136
  request: _RequestQ[Q]
117
137
 
118
138
 
@@ -121,7 +141,7 @@ class _RequestH(TypedDict, Generic[H]):
121
141
  cookies: NotRequired[type]
122
142
 
123
143
 
124
- class _RouteH(_OperationConfig[R], Generic[H, R]):
144
+ class _RouteH(_OperationConfig[R, K], Generic[H, R, K]):
125
145
  request: _RequestH[H]
126
146
 
127
147
 
@@ -130,7 +150,7 @@ class _RequestB(TypedDict, Generic[J]):
130
150
  cookies: NotRequired[type]
131
151
 
132
152
 
133
- class _RouteB(_OperationConfig[R], Generic[J, R]):
153
+ class _RouteB(_OperationConfig[R, K], Generic[J, R, K]):
134
154
  request: _RequestB[J]
135
155
 
136
156
 
@@ -140,7 +160,7 @@ class _RequestPQ(TypedDict, Generic[P, Q]):
140
160
  cookies: NotRequired[type]
141
161
 
142
162
 
143
- class _RoutePQ(_OperationConfig[R], Generic[P, Q, R]):
163
+ class _RoutePQ(_OperationConfig[R, K], Generic[P, Q, R, K]):
144
164
  request: _RequestPQ[P, Q]
145
165
 
146
166
 
@@ -150,7 +170,7 @@ class _RequestPH(TypedDict, Generic[P, H]):
150
170
  cookies: NotRequired[type]
151
171
 
152
172
 
153
- class _RoutePH(_OperationConfig[R], Generic[P, H, R]):
173
+ class _RoutePH(_OperationConfig[R, K], Generic[P, H, R, K]):
154
174
  request: _RequestPH[P, H]
155
175
 
156
176
 
@@ -160,7 +180,7 @@ class _RequestPB(TypedDict, Generic[P, J]):
160
180
  cookies: NotRequired[type]
161
181
 
162
182
 
163
- class _RoutePB(_OperationConfig[R], Generic[P, J, R]):
183
+ class _RoutePB(_OperationConfig[R, K], Generic[P, J, R, K]):
164
184
  request: _RequestPB[P, J]
165
185
 
166
186
 
@@ -170,7 +190,7 @@ class _RequestQH(TypedDict, Generic[Q, H]):
170
190
  cookies: NotRequired[type]
171
191
 
172
192
 
173
- class _RouteQH(_OperationConfig[R], Generic[Q, H, R]):
193
+ class _RouteQH(_OperationConfig[R, K], Generic[Q, H, R, K]):
174
194
  request: _RequestQH[Q, H]
175
195
 
176
196
 
@@ -180,7 +200,7 @@ class _RequestQB(TypedDict, Generic[Q, J]):
180
200
  cookies: NotRequired[type]
181
201
 
182
202
 
183
- class _RouteQB(_OperationConfig[R], Generic[Q, J, R]):
203
+ class _RouteQB(_OperationConfig[R, K], Generic[Q, J, R, K]):
184
204
  request: _RequestQB[Q, J]
185
205
 
186
206
 
@@ -190,7 +210,7 @@ class _RequestHB(TypedDict, Generic[J, H]):
190
210
  cookies: NotRequired[type]
191
211
 
192
212
 
193
- class _RouteHB(_OperationConfig[R], Generic[J, H, R]):
213
+ class _RouteHB(_OperationConfig[R, K], Generic[J, H, R, K]):
194
214
  request: _RequestHB[J, H]
195
215
 
196
216
 
@@ -201,7 +221,7 @@ class _RequestPQH(TypedDict, Generic[P, Q, H]):
201
221
  cookies: NotRequired[type]
202
222
 
203
223
 
204
- class _RoutePQH(_OperationConfig[R], Generic[P, Q, H, R]):
224
+ class _RoutePQH(_OperationConfig[R, K], Generic[P, Q, H, R, K]):
205
225
  request: _RequestPQH[P, Q, H]
206
226
 
207
227
 
@@ -212,7 +232,7 @@ class _RequestPQB(TypedDict, Generic[P, Q, J]):
212
232
  cookies: NotRequired[type]
213
233
 
214
234
 
215
- class _RoutePQB(_OperationConfig[R], Generic[P, Q, J, R]):
235
+ class _RoutePQB(_OperationConfig[R, K], Generic[P, Q, J, R, K]):
216
236
  request: _RequestPQB[P, Q, J]
217
237
 
218
238
 
@@ -223,7 +243,7 @@ class _RequestPHB(TypedDict, Generic[P, J, H]):
223
243
  cookies: NotRequired[type]
224
244
 
225
245
 
226
- class _RoutePHB(_OperationConfig[R], Generic[P, J, H, R]):
246
+ class _RoutePHB(_OperationConfig[R, K], Generic[P, J, H, R, K]):
227
247
  request: _RequestPHB[P, J, H]
228
248
 
229
249
 
@@ -234,7 +254,7 @@ class _RequestQHB(TypedDict, Generic[Q, J, H]):
234
254
  cookies: NotRequired[type]
235
255
 
236
256
 
237
- class _RouteQHB(_OperationConfig[R], Generic[Q, J, H, R]):
257
+ class _RouteQHB(_OperationConfig[R, K], Generic[Q, J, H, R, K]):
238
258
  request: _RequestQHB[Q, J, H]
239
259
 
240
260
 
@@ -246,11 +266,11 @@ class _RequestPQHB(TypedDict, Generic[P, Q, J, H]):
246
266
  cookies: NotRequired[type]
247
267
 
248
268
 
249
- class _RoutePQHB(_OperationConfig[R], Generic[P, Q, J, H, R]):
269
+ class _RoutePQHB(_OperationConfig[R, K], Generic[P, Q, J, H, R, K]):
250
270
  request: _RequestPQHB[P, Q, J, H]
251
271
 
252
272
 
253
- class Route(Generic[P, Q, J, H, R]):
273
+ class Route(Generic[P, Q, J, H, R, S]):
254
274
  """An operation made by :func:`createRoute`. Declare it as a class, so that handlers can name
255
275
  it::
256
276
 
@@ -279,7 +299,7 @@ class Route(Generic[P, Q, J, H, R]):
279
299
  raise TypeError("routes are classes: use the route itself (app.openapi(route, handler))")
280
300
 
281
301
 
282
- AnyRoute = Route[Any, Any, Any, Any, Any]
302
+ AnyRoute = Route[Any, Any, Any, Any, Any, Any]
283
303
 
284
304
 
285
305
  def route_class(config: Mapping[str, Any], *, generated: bool = True) -> type[AnyRoute]:
@@ -294,109 +314,110 @@ _Q = TypeVar("_Q")
294
314
  _J = TypeVar("_J")
295
315
  _H = TypeVar("_H")
296
316
  _R = TypeVar("_R")
317
+ _K = TypeVar("_K", bound=StatusCode | Literal["default"])
297
318
 
298
319
 
299
320
  @overload
300
- def createRoute(config: _OperationConfig[_R]) -> type[Route[Any, Any, Any, Any, _R]]: ...
321
+ def createRoute(config: _OperationConfig[_R, _K]) -> type[Route[Any, Any, Any, Any, _R, _K]]: ...
301
322
 
302
323
 
303
324
  # A dict literal with a "request" key cannot match the overload above (TypedDicts reject extra
304
325
  # keys), although pyright's overlap check treats the shapes below as its structural subtypes.
305
326
  @overload
306
327
  def createRoute( # pyright: ignore[reportOverlappingOverload]
307
- config: _RouteP[_P, _R],
308
- ) -> type[Route[_P, Any, Any, Any, _R]]: ...
328
+ config: _RouteP[_P, _R, _K],
329
+ ) -> type[Route[_P, Any, Any, Any, _R, _K]]: ...
309
330
 
310
331
 
311
332
  @overload
312
333
  def createRoute( # pyright: ignore[reportOverlappingOverload]
313
- config: _RouteQ[_Q, _R],
314
- ) -> type[Route[Any, _Q, Any, Any, _R]]: ...
334
+ config: _RouteQ[_Q, _R, _K],
335
+ ) -> type[Route[Any, _Q, Any, Any, _R, _K]]: ...
315
336
 
316
337
 
317
338
  @overload
318
339
  def createRoute( # pyright: ignore[reportOverlappingOverload]
319
- config: _RouteH[_H, _R],
320
- ) -> type[Route[Any, Any, Any, _H, _R]]: ...
340
+ config: _RouteH[_H, _R, _K],
341
+ ) -> type[Route[Any, Any, Any, _H, _R, _K]]: ...
321
342
 
322
343
 
323
344
  @overload
324
345
  def createRoute( # pyright: ignore[reportOverlappingOverload]
325
- config: _RouteB[_J, _R],
326
- ) -> type[Route[Any, Any, _J, Any, _R]]: ...
346
+ config: _RouteB[_J, _R, _K],
347
+ ) -> type[Route[Any, Any, _J, Any, _R, _K]]: ...
327
348
 
328
349
 
329
350
  @overload
330
351
  def createRoute( # pyright: ignore[reportOverlappingOverload]
331
- config: _RoutePQ[_P, _Q, _R],
332
- ) -> type[Route[_P, _Q, Any, Any, _R]]: ...
352
+ config: _RoutePQ[_P, _Q, _R, _K],
353
+ ) -> type[Route[_P, _Q, Any, Any, _R, _K]]: ...
333
354
 
334
355
 
335
356
  @overload
336
357
  def createRoute( # pyright: ignore[reportOverlappingOverload]
337
- config: _RoutePH[_P, _H, _R],
338
- ) -> type[Route[_P, Any, Any, _H, _R]]: ...
358
+ config: _RoutePH[_P, _H, _R, _K],
359
+ ) -> type[Route[_P, Any, Any, _H, _R, _K]]: ...
339
360
 
340
361
 
341
362
  @overload
342
363
  def createRoute( # pyright: ignore[reportOverlappingOverload]
343
- config: _RoutePB[_P, _J, _R],
344
- ) -> type[Route[_P, Any, _J, Any, _R]]: ...
364
+ config: _RoutePB[_P, _J, _R, _K],
365
+ ) -> type[Route[_P, Any, _J, Any, _R, _K]]: ...
345
366
 
346
367
 
347
368
  @overload
348
369
  def createRoute( # pyright: ignore[reportOverlappingOverload]
349
- config: _RouteQH[_Q, _H, _R],
350
- ) -> type[Route[Any, _Q, Any, _H, _R]]: ...
370
+ config: _RouteQH[_Q, _H, _R, _K],
371
+ ) -> type[Route[Any, _Q, Any, _H, _R, _K]]: ...
351
372
 
352
373
 
353
374
  @overload
354
375
  def createRoute( # pyright: ignore[reportOverlappingOverload]
355
- config: _RouteQB[_Q, _J, _R],
356
- ) -> type[Route[Any, _Q, _J, Any, _R]]: ...
376
+ config: _RouteQB[_Q, _J, _R, _K],
377
+ ) -> type[Route[Any, _Q, _J, Any, _R, _K]]: ...
357
378
 
358
379
 
359
380
  @overload
360
381
  def createRoute( # pyright: ignore[reportOverlappingOverload]
361
- config: _RouteHB[_J, _H, _R],
362
- ) -> type[Route[Any, Any, _J, _H, _R]]: ...
382
+ config: _RouteHB[_J, _H, _R, _K],
383
+ ) -> type[Route[Any, Any, _J, _H, _R, _K]]: ...
363
384
 
364
385
 
365
386
  @overload
366
387
  def createRoute( # pyright: ignore[reportOverlappingOverload]
367
- config: _RoutePQH[_P, _Q, _H, _R],
368
- ) -> type[Route[_P, _Q, Any, _H, _R]]: ...
388
+ config: _RoutePQH[_P, _Q, _H, _R, _K],
389
+ ) -> type[Route[_P, _Q, Any, _H, _R, _K]]: ...
369
390
 
370
391
 
371
392
  @overload
372
393
  def createRoute( # pyright: ignore[reportOverlappingOverload]
373
- config: _RoutePQB[_P, _Q, _J, _R],
374
- ) -> type[Route[_P, _Q, _J, Any, _R]]: ...
394
+ config: _RoutePQB[_P, _Q, _J, _R, _K],
395
+ ) -> type[Route[_P, _Q, _J, Any, _R, _K]]: ...
375
396
 
376
397
 
377
398
  @overload
378
399
  def createRoute( # pyright: ignore[reportOverlappingOverload]
379
- config: _RoutePHB[_P, _J, _H, _R],
380
- ) -> type[Route[_P, Any, _J, _H, _R]]: ...
400
+ config: _RoutePHB[_P, _J, _H, _R, _K],
401
+ ) -> type[Route[_P, Any, _J, _H, _R, _K]]: ...
381
402
 
382
403
 
383
404
  @overload
384
405
  def createRoute( # pyright: ignore[reportOverlappingOverload]
385
- config: _RouteQHB[_Q, _J, _H, _R],
386
- ) -> type[Route[Any, _Q, _J, _H, _R]]: ...
406
+ config: _RouteQHB[_Q, _J, _H, _R, _K],
407
+ ) -> type[Route[Any, _Q, _J, _H, _R, _K]]: ...
387
408
 
388
409
 
389
410
  @overload
390
411
  def createRoute( # pyright: ignore[reportOverlappingOverload]
391
- config: _RoutePQHB[_P, _Q, _J, _H, _R],
392
- ) -> type[Route[_P, _Q, _J, _H, _R]]: ...
412
+ config: _RoutePQHB[_P, _Q, _J, _H, _R, _K],
413
+ ) -> type[Route[_P, _Q, _J, _H, _R, _K]]: ...
393
414
 
394
415
 
395
416
  # Anything else (cookies only, a dict built at run time).
396
417
  @overload
397
418
  def createRoute( # pyright: ignore[reportOverlappingOverload]
398
- config: RouteConfig[_P, _Q, _J, _H, _R],
399
- ) -> type[Route[_P, _Q, _J, _H, _R]]: ...
419
+ config: RouteConfig[_P, _Q, _J, _H, _R, _K],
420
+ ) -> type[Route[_P, _Q, _J, _H, _R, _K]]: ...
400
421
 
401
422
 
402
423
  def createRoute(config: Any) -> Any: # noqa: N802 (the conventional name)
@@ -405,4 +426,13 @@ def createRoute(config: Any) -> Any: # noqa: N802 (the conventional name)
405
426
  return route_class(config)
406
427
 
407
428
 
408
- __all__ = ["AnyRoute", "RequestConfig", "ResponseConfig", "Route", "RouteConfig", "SchemaLike", "createRoute"]
429
+ __all__ = [
430
+ "AnyRoute",
431
+ "RequestConfig",
432
+ "ResponseConfig",
433
+ "Route",
434
+ "RouteConfig",
435
+ "SchemaLike",
436
+ "StatusCode",
437
+ "createRoute",
438
+ ]
@@ -1,25 +0,0 @@
1
- load("//tools/pytest:defs.bzl", "pytest_test")
2
-
3
- # The Rust engine's sources, built into takibiapi._core by //pypi/takibiapi/python:core.
4
- filegroup(
5
- name = "core_srcs",
6
- srcs = glob(["src/**/*.rs"]),
7
- visibility = ["//pypi/takibiapi/python:__pkg__"],
8
- )
9
-
10
- exports_files(
11
- ["src/lib.rs"],
12
- visibility = ["//pypi/takibiapi/python:__pkg__"],
13
- )
14
-
15
- # tests/test_typing.py is not here: it shells out to pyright, which `just typecheck` runs over
16
- # tests/typing directly.
17
- pytest_test(
18
- name = "test",
19
- size = "small",
20
- tests = glob(
21
- ["tests/test_*.py"],
22
- exclude = ["tests/test_typing.py"],
23
- ),
24
- deps = ["//pypi/takibiapi/python:takibiapi"],
25
- )
@@ -1,7 +0,0 @@
1
- # takibiapi
2
-
3
- ## 0.1.0
4
-
5
- ### Minor Changes
6
-
7
- - Initial release
@@ -1,29 +0,0 @@
1
- load("@rules_python//python:defs.bzl", "py_library")
2
- load("@rules_rust_pyo3//:defs.bzl", "pyo3_extension")
3
-
4
- # takibiapi (`import takibiapi`): the framework. `imports = ["."]` makes the import resolve from
5
- # this directory, as when the wheel is installed.
6
- py_library(
7
- name = "takibiapi",
8
- srcs = glob(["takibiapi/*.py"]),
9
- data = [
10
- "takibiapi/_core.pyi",
11
- "takibiapi/py.typed",
12
- ],
13
- imports = ["."],
14
- visibility = ["//visibility:public"],
15
- deps = [":core"],
16
- )
17
-
18
- # takibiapi._core: the Rust engine (request checks, JSON, routing), placed next to the Python
19
- # sources as takibiapi/_core.so. takibiapi.schema falls back to its Python checker without it.
20
- pyo3_extension(
21
- name = "core",
22
- srcs = ["//pypi/takibiapi:core_srcs"],
23
- crate_name = "_core",
24
- crate_root = "//pypi/takibiapi:src/lib.rs",
25
- edition = "2021",
26
- imports = ["."],
27
- module_name = "takibiapi._core",
28
- stubs = False,
29
- )
@@ -1,97 +0,0 @@
1
- # pyright: reportUnnecessaryTypeIgnoreComment=true
2
- """Static types of routes and handlers (checked by pyright; see typing_test.py)."""
3
-
4
- from dataclasses import dataclass
5
- from typing import TypedDict, reveal_type
6
-
7
- from takibiapi import Context, TakibiAPI, createRoute
8
-
9
-
10
- class TodoParams(TypedDict):
11
- todoId: int
12
-
13
-
14
- class Search(TypedDict):
15
- q: str
16
-
17
-
18
- @dataclass
19
- class NewTodo:
20
- title: str
21
-
22
-
23
- @dataclass
24
- class Todo:
25
- id: int
26
- title: str
27
-
28
-
29
- @dataclass
30
- class Problem:
31
- detail: str
32
-
33
-
34
- @dataclass
35
- class Other:
36
- x: int
37
-
38
-
39
- class putTodoRoute(
40
- createRoute(
41
- {
42
- "method": "put",
43
- "path": "/todos/{todoId}",
44
- "request": {
45
- "params": TodoParams,
46
- "query": Search,
47
- "body": {"content": {"application/json": {"schema": NewTodo}}},
48
- },
49
- "responses": {
50
- 200: {"description": "ok", "content": {"application/json": {"schema": Todo}}},
51
- 404: {"description": "missing", "content": {"application/problem+json": {"schema": Problem}}},
52
- },
53
- }
54
- )
55
- ):
56
- """Update a todo."""
57
-
58
-
59
- async def putTodoRouteHandler(c: Context[putTodoRoute]):
60
- body = c.req.valid("json")
61
- reveal_type(body, expected_text="NewTodo")
62
- reveal_type(c.req.valid("param")["todoId"], expected_text="int")
63
- reveal_type(c.req.valid("query"), expected_text="Search")
64
- c.req.valid("header") # no headers type: Any
65
- reveal_type(await c.req.json(), expected_text="Any") # the raw, unchecked body
66
- reveal_type(c.req.param("todoId"), expected_text="str") # the raw text
67
- reveal_type(c.req.query("q"), expected_text="str | None")
68
- reveal_type(c.req.queries("tag"), expected_text="list[str]")
69
- c.json(Other(x=1)) # pyright: ignore[reportArgumentType] # not a declared response body
70
- c.json({"id": 1, "title": "x"}) # pyright: ignore[reportArgumentType] # a dict is not a Todo
71
- c.json(Problem(detail="x"), 404)
72
- params = c.req.valid("param")
73
- return c.json(Todo(id=params["todoId"], title=body.title), 200)
74
-
75
-
76
- class getOtherRoute(
77
- createRoute(
78
- {
79
- "method": "get",
80
- "path": "/other",
81
- "responses": {200: {"description": "ok", "content": {"application/json": {"schema": list[Search]}}}},
82
- }
83
- )
84
- ):
85
- pass
86
-
87
-
88
- def getOtherRouteHandler(c: Context[getOtherRoute]):
89
- c.json([{"q": "x"}])
90
- c.json([{"name": "x"}]) # pyright: ignore[reportArgumentType] # wrong TypedDict key
91
- return c.json([])
92
-
93
-
94
- app = TakibiAPI()
95
- app.openapi(putTodoRoute, putTodoRouteHandler)
96
- app.openapi(getOtherRoute, getOtherRouteHandler)
97
- app.openapi(putTodoRoute, getOtherRouteHandler) # pyright: ignore[reportArgumentType] # another route's handler
File without changes
File without changes