takibiapi 0.1.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 (41) hide show
  1. takibiapi-0.1.0/Cargo.lock +133 -0
  2. takibiapi-0.1.0/Cargo.toml +23 -0
  3. takibiapi-0.1.0/LICENSE +21 -0
  4. takibiapi-0.1.0/PKG-INFO +152 -0
  5. takibiapi-0.1.0/README.md +128 -0
  6. takibiapi-0.1.0/pypi/takibiapi/BUILD.bazel +25 -0
  7. takibiapi-0.1.0/pypi/takibiapi/CHANGELOG.md +7 -0
  8. takibiapi-0.1.0/pypi/takibiapi/Cargo.toml +20 -0
  9. takibiapi-0.1.0/pypi/takibiapi/LICENSE +21 -0
  10. takibiapi-0.1.0/pypi/takibiapi/README.md +128 -0
  11. takibiapi-0.1.0/pypi/takibiapi/build.rs +16 -0
  12. takibiapi-0.1.0/pypi/takibiapi/python/BUILD.bazel +29 -0
  13. takibiapi-0.1.0/pypi/takibiapi/src/encode.rs +236 -0
  14. takibiapi-0.1.0/pypi/takibiapi/src/engine.rs +647 -0
  15. takibiapi-0.1.0/pypi/takibiapi/src/env.rs +235 -0
  16. takibiapi-0.1.0/pypi/takibiapi/src/json.rs +368 -0
  17. takibiapi-0.1.0/pypi/takibiapi/src/lib.rs +192 -0
  18. takibiapi-0.1.0/pypi/takibiapi/src/plan.rs +255 -0
  19. takibiapi-0.1.0/pypi/takibiapi/src/router.rs +162 -0
  20. takibiapi-0.1.0/pypi/takibiapi/tests/test_app.py +586 -0
  21. takibiapi-0.1.0/pypi/takibiapi/tests/test_cookie.py +198 -0
  22. takibiapi-0.1.0/pypi/takibiapi/tests/test_cors.py +135 -0
  23. takibiapi-0.1.0/pypi/takibiapi/tests/test_json.py +297 -0
  24. takibiapi-0.1.0/pypi/takibiapi/tests/test_router.py +91 -0
  25. takibiapi-0.1.0/pypi/takibiapi/tests/test_schema.py +292 -0
  26. takibiapi-0.1.0/pypi/takibiapi/tests/test_typing.py +28 -0
  27. takibiapi-0.1.0/pypi/takibiapi/tests/typing/routes.py +97 -0
  28. takibiapi-0.1.0/pyproject.toml +37 -0
  29. takibiapi-0.1.0/python/takibiapi/__init__.py +41 -0
  30. takibiapi-0.1.0/python/takibiapi/_core.pyi +22 -0
  31. takibiapi-0.1.0/python/takibiapi/app.py +451 -0
  32. takibiapi-0.1.0/python/takibiapi/context.py +317 -0
  33. takibiapi-0.1.0/python/takibiapi/cookie.py +282 -0
  34. takibiapi-0.1.0/python/takibiapi/cors.py +112 -0
  35. takibiapi-0.1.0/python/takibiapi/openapi.py +98 -0
  36. takibiapi-0.1.0/python/takibiapi/problems.py +34 -0
  37. takibiapi-0.1.0/python/takibiapi/py.typed +0 -0
  38. takibiapi-0.1.0/python/takibiapi/route.py +408 -0
  39. takibiapi-0.1.0/python/takibiapi/router.py +115 -0
  40. takibiapi-0.1.0/python/takibiapi/schema.py +582 -0
  41. takibiapi-0.1.0/python/takibiapi/schemas.py +109 -0
@@ -0,0 +1,133 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "heck"
7
+ version = "0.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
10
+
11
+ [[package]]
12
+ name = "libc"
13
+ version = "0.2.191"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
16
+
17
+ [[package]]
18
+ name = "once_cell"
19
+ version = "1.21.4"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
22
+
23
+ [[package]]
24
+ name = "portable-atomic"
25
+ version = "1.15.0"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
28
+
29
+ [[package]]
30
+ name = "proc-macro2"
31
+ version = "1.0.107"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
34
+ dependencies = [
35
+ "unicode-ident",
36
+ ]
37
+
38
+ [[package]]
39
+ name = "pyo3"
40
+ version = "0.29.3"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
43
+ dependencies = [
44
+ "libc",
45
+ "once_cell",
46
+ "portable-atomic",
47
+ "pyo3-build-config",
48
+ "pyo3-ffi",
49
+ "pyo3-macros",
50
+ ]
51
+
52
+ [[package]]
53
+ name = "pyo3-build-config"
54
+ version = "0.29.3"
55
+ source = "registry+https://github.com/rust-lang/crates.io-index"
56
+ checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
57
+ dependencies = [
58
+ "target-lexicon",
59
+ ]
60
+
61
+ [[package]]
62
+ name = "pyo3-ffi"
63
+ version = "0.29.3"
64
+ source = "registry+https://github.com/rust-lang/crates.io-index"
65
+ checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
66
+ dependencies = [
67
+ "libc",
68
+ "pyo3-build-config",
69
+ ]
70
+
71
+ [[package]]
72
+ name = "pyo3-macros"
73
+ version = "0.29.3"
74
+ source = "registry+https://github.com/rust-lang/crates.io-index"
75
+ checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
76
+ dependencies = [
77
+ "proc-macro2",
78
+ "pyo3-macros-backend",
79
+ "quote",
80
+ "syn",
81
+ ]
82
+
83
+ [[package]]
84
+ name = "pyo3-macros-backend"
85
+ version = "0.29.3"
86
+ source = "registry+https://github.com/rust-lang/crates.io-index"
87
+ checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
88
+ dependencies = [
89
+ "heck",
90
+ "proc-macro2",
91
+ "quote",
92
+ "syn",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "quote"
97
+ version = "1.0.47"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
100
+ dependencies = [
101
+ "proc-macro2",
102
+ ]
103
+
104
+ [[package]]
105
+ name = "syn"
106
+ version = "2.0.119"
107
+ source = "registry+https://github.com/rust-lang/crates.io-index"
108
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
109
+ dependencies = [
110
+ "proc-macro2",
111
+ "quote",
112
+ "unicode-ident",
113
+ ]
114
+
115
+ [[package]]
116
+ name = "takibiapi"
117
+ version = "0.0.0"
118
+ dependencies = [
119
+ "pyo3",
120
+ "pyo3-build-config",
121
+ ]
122
+
123
+ [[package]]
124
+ name = "target-lexicon"
125
+ version = "0.13.5"
126
+ source = "registry+https://github.com/rust-lang/crates.io-index"
127
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
128
+
129
+ [[package]]
130
+ name = "unicode-ident"
131
+ version = "1.0.27"
132
+ source = "registry+https://github.com/rust-lang/crates.io-index"
133
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
@@ -0,0 +1,23 @@
1
+ # Cargo workspace. The Rust extension crate lives next to its pyproject.toml in pypi/takibiapi,
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.
6
+ [workspace]
7
+ resolver = "2"
8
+ members = ["pypi/takibiapi"]
9
+
10
+ [workspace.package]
11
+ edition = "2021"
12
+ rust-version = "1.83"
13
+ license = "MIT"
14
+ repository = "https://github.com/nakita628/takibi"
15
+
16
+ [workspace.dependencies]
17
+ pyo3 = "0.29"
18
+ pyo3-build-config = { version = "0.29", features = ["resolve-config"] }
19
+
20
+ [profile.release]
21
+ lto = "fat"
22
+ codegen-units = 1
23
+ opt-level = 3
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 - present, takibiapi Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: takibiapi
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Operating System :: OS Independent
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: Programming Language :: Rust
12
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
13
+ Classifier: Typing :: Typed
14
+ License-File: LICENSE
15
+ Summary: REST APIs whose OpenAPI document, request checks and handler types come from Python's own types
16
+ Keywords: openapi,asgi,rsgi,rest,api,typing,dataclasses,typeddict
17
+ Author: nakita628
18
+ License-Expression: MIT
19
+ Requires-Python: >=3.12
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
23
+
24
+ # TakibiAPI
25
+
26
+ REST APIs whose OpenAPI document, request checks and handler types come from Python's own types (dataclasses, `TypedDict`, `Literal`, `Enum`, `Annotated`). No runtime dependencies; JSON, routing and type checks run in a Rust extension. ASGI and Granian's RSGI.
27
+
28
+ ```sh
29
+ pip install takibiapi
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from dataclasses import dataclass
36
+
37
+ from takibiapi import Context, TakibiAPI, createRoute
38
+
39
+
40
+ @dataclass
41
+ class Message:
42
+ message: str
43
+
44
+
45
+ class getTakibiRoute(
46
+ createRoute(
47
+ {
48
+ "method": "get",
49
+ "path": "/takibi",
50
+ "responses": {200: {"description": "takibišŸ”„", "content": {"application/json": {"schema": Message}}}},
51
+ }
52
+ )
53
+ ):
54
+ """Say takibišŸ”„."""
55
+
56
+
57
+ def getTakibiRouteHandler(c: Context[getTakibiRoute]):
58
+ return c.json(Message("takibišŸ”„"))
59
+
60
+
61
+ app = TakibiAPI()
62
+ app.openapi(getTakibiRoute, getTakibiRouteHandler)
63
+ app.doc("/doc", {"openapi": "3.1.0", "info": {"title": "takibi API", "version": "1.0.0"}})
64
+ ```
65
+
66
+ ```bash
67
+ granian --interface rsgi myapp:app # or: uvicorn myapp:app
68
+ curl localhost:8000/takibi # {"message":"takibišŸ”„"}
69
+ ```
70
+
71
+ ## Routes and handlers
72
+
73
+ 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
+ ## Requests
76
+
77
+ 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")`:
78
+
79
+ ```python
80
+ params = c.req.valid("param") # "/todos/7" -> {"todoId": 7}: text converted to the declared int
81
+ ```
82
+
83
+ Only types are checked: `Annotated` keywords (`minLength`, `maximum`, ...) are written to the document but not checked, and responses are not checked at run time. For those, use [pydantic-takibiapi](../pydantic-takibiapi) or [zopy-takibiapi](../zopy-takibiapi).
84
+
85
+ A wrong type gets **422** Problem Details (`errors: [{in, path, code, message}]`), invalid JSON **400**, a required body with another Content-Type **415**.
86
+
87
+ ## Errors
88
+
89
+ ```python
90
+ app.onError(lambda c: c.json({"error": str(c.error)}, 500)) # exceptions from handlers and middleware
91
+ app.notFound(lambda c: c.text("not here", 404)) # paths with no route
92
+ ```
93
+
94
+ Both take handlers `def h(c)` (the exception is `c.error`) and middleware `async def mw(c, next)`, and registrations compose instead of replacing each other: middleware runs around the handlers registered after it (`await next()` is their response, `None` when none answered), a handler ends the chain by returning a response, and `prefix="/api"` limits a registration to those paths. The handlers of a mounted app (`app.route("/v1", v1)`) come first for its paths, then the parent's in registration order.
95
+
96
+ ```python
97
+ async def report(c, next):
98
+ res = await next()
99
+ log.warning("%s %s -> %s", c.req.method, c.req.path, res.status if res else "default")
100
+ return res
101
+
102
+
103
+ app.onError(report, lambda c: c.text("API failed", 500), prefix="/api")
104
+ app.notFound(report, lambda c: c.text("no such API", 404), prefix="/api")
105
+ ```
106
+
107
+ Without them, Takibi answers 500 / 404 Problem Details (and 405 with `Allow` for a known path), which also answer when a chain ends without a response.
108
+
109
+ ## Middleware and CORS
110
+
111
+ `app.use(middleware, prefix="/api")` runs `async def mw(c, next)` around the handlers. CORS, as Hono's `hono/cors`:
112
+
113
+ ```python
114
+ from takibiapi.cors import cors
115
+
116
+ app.use(cors()) # Access-Control-Allow-Origin: *
117
+ app.use(cors({"origin": ["https://a.example"], "credentials": True, "maxAge": 600}), prefix="/api")
118
+ ```
119
+
120
+ Options: `origin` (`"*"`, an origin, a list, or `(origin, c) -> origin | None`), `allowMethods`, `allowHeaders`, `exposeHeaders`, `maxAge`, `credentials`. Preflights (`OPTIONS`) are answered with 204.
121
+
122
+ ## Cookies
123
+
124
+ `takibiapi.cookie` reads and sets cookies, plain or signed:
125
+
126
+ ```python
127
+ from takibiapi.cookie import deleteCookie, getCookie, getSignedCookie, setCookie, setSignedCookie
128
+
129
+
130
+ def getProfileRouteHandler(c: Context[getProfileRoute]):
131
+ theme = getCookie(c, "theme") # str | None; getCookie(c) is every cookie as a dict
132
+ session = getSignedCookie(c, secret, "session") # the value, False if tampered with, None if absent
133
+ setCookie(c, "theme", "dark", {"maxAge": 86400, "httpOnly": True, "sameSite": "Lax"})
134
+ setSignedCookie(c, "session", "42", secret)
135
+ deleteCookie(c, "old") # Max-Age=0; returns the value the request carried
136
+ return c.json(profile)
137
+ ```
138
+
139
+ `setCookie` adds a `Set-Cookie` header to the response the handler returns (`c.header(name, value, append=True)` underneath); `path` is `/` unless given, and the options are the header's attributes (`domain`, `expires`, `httpOnly`, `maxAge`, `path`, `secure`, `sameSite`, `priority`, `partitioned`) plus `prefix`: `"secure"` names the cookie `__Secure-<name>`, `"host"` `__Host-<name>`, each with the attributes it requires. A signed cookie carries an HMAC-SHA256 of its value, in the format Takibi (npm) uses, so the two read each other's cookies with the same secret. `generateCookie` / `generateSignedCookie` give the header value alone.
140
+
141
+ ## OpenAPI
142
+
143
+ `app.doc(path, config)` serves the document and `app.getOpenAPIDocument(config)` returns it; `config` must give `openapi` (3.0.x or 3.1.x) and `info`. Dataclasses, `TypedDict`s and `Enum`s become `components.schemas`. Swagger UI: [takibi-swagger-ui](../takibi-swagger-ui).
144
+
145
+ ## Development
146
+
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.
148
+
149
+ ## License
150
+
151
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/takibiapi/LICENSE) for more information.
152
+
@@ -0,0 +1,128 @@
1
+ # TakibiAPI
2
+
3
+ REST APIs whose OpenAPI document, request checks and handler types come from Python's own types (dataclasses, `TypedDict`, `Literal`, `Enum`, `Annotated`). No runtime dependencies; JSON, routing and type checks run in a Rust extension. ASGI and Granian's RSGI.
4
+
5
+ ```sh
6
+ pip install takibiapi
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ from dataclasses import dataclass
13
+
14
+ from takibiapi import Context, TakibiAPI, createRoute
15
+
16
+
17
+ @dataclass
18
+ class Message:
19
+ message: str
20
+
21
+
22
+ class getTakibiRoute(
23
+ createRoute(
24
+ {
25
+ "method": "get",
26
+ "path": "/takibi",
27
+ "responses": {200: {"description": "takibišŸ”„", "content": {"application/json": {"schema": Message}}}},
28
+ }
29
+ )
30
+ ):
31
+ """Say takibišŸ”„."""
32
+
33
+
34
+ def getTakibiRouteHandler(c: Context[getTakibiRoute]):
35
+ return c.json(Message("takibišŸ”„"))
36
+
37
+
38
+ app = TakibiAPI()
39
+ app.openapi(getTakibiRoute, getTakibiRouteHandler)
40
+ app.doc("/doc", {"openapi": "3.1.0", "info": {"title": "takibi API", "version": "1.0.0"}})
41
+ ```
42
+
43
+ ```bash
44
+ granian --interface rsgi myapp:app # or: uvicorn myapp:app
45
+ curl localhost:8000/takibi # {"message":"takibišŸ”„"}
46
+ ```
47
+
48
+ ## Routes and handlers
49
+
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
+
52
+ ## Requests
53
+
54
+ 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")`:
55
+
56
+ ```python
57
+ params = c.req.valid("param") # "/todos/7" -> {"todoId": 7}: text converted to the declared int
58
+ ```
59
+
60
+ Only types are checked: `Annotated` keywords (`minLength`, `maximum`, ...) are written to the document but not checked, and responses are not checked at run time. For those, use [pydantic-takibiapi](../pydantic-takibiapi) or [zopy-takibiapi](../zopy-takibiapi).
61
+
62
+ A wrong type gets **422** Problem Details (`errors: [{in, path, code, message}]`), invalid JSON **400**, a required body with another Content-Type **415**.
63
+
64
+ ## Errors
65
+
66
+ ```python
67
+ app.onError(lambda c: c.json({"error": str(c.error)}, 500)) # exceptions from handlers and middleware
68
+ app.notFound(lambda c: c.text("not here", 404)) # paths with no route
69
+ ```
70
+
71
+ Both take handlers `def h(c)` (the exception is `c.error`) and middleware `async def mw(c, next)`, and registrations compose instead of replacing each other: middleware runs around the handlers registered after it (`await next()` is their response, `None` when none answered), a handler ends the chain by returning a response, and `prefix="/api"` limits a registration to those paths. The handlers of a mounted app (`app.route("/v1", v1)`) come first for its paths, then the parent's in registration order.
72
+
73
+ ```python
74
+ async def report(c, next):
75
+ res = await next()
76
+ log.warning("%s %s -> %s", c.req.method, c.req.path, res.status if res else "default")
77
+ return res
78
+
79
+
80
+ app.onError(report, lambda c: c.text("API failed", 500), prefix="/api")
81
+ app.notFound(report, lambda c: c.text("no such API", 404), prefix="/api")
82
+ ```
83
+
84
+ Without them, Takibi answers 500 / 404 Problem Details (and 405 with `Allow` for a known path), which also answer when a chain ends without a response.
85
+
86
+ ## Middleware and CORS
87
+
88
+ `app.use(middleware, prefix="/api")` runs `async def mw(c, next)` around the handlers. CORS, as Hono's `hono/cors`:
89
+
90
+ ```python
91
+ from takibiapi.cors import cors
92
+
93
+ app.use(cors()) # Access-Control-Allow-Origin: *
94
+ app.use(cors({"origin": ["https://a.example"], "credentials": True, "maxAge": 600}), prefix="/api")
95
+ ```
96
+
97
+ Options: `origin` (`"*"`, an origin, a list, or `(origin, c) -> origin | None`), `allowMethods`, `allowHeaders`, `exposeHeaders`, `maxAge`, `credentials`. Preflights (`OPTIONS`) are answered with 204.
98
+
99
+ ## Cookies
100
+
101
+ `takibiapi.cookie` reads and sets cookies, plain or signed:
102
+
103
+ ```python
104
+ from takibiapi.cookie import deleteCookie, getCookie, getSignedCookie, setCookie, setSignedCookie
105
+
106
+
107
+ def getProfileRouteHandler(c: Context[getProfileRoute]):
108
+ theme = getCookie(c, "theme") # str | None; getCookie(c) is every cookie as a dict
109
+ session = getSignedCookie(c, secret, "session") # the value, False if tampered with, None if absent
110
+ setCookie(c, "theme", "dark", {"maxAge": 86400, "httpOnly": True, "sameSite": "Lax"})
111
+ setSignedCookie(c, "session", "42", secret)
112
+ deleteCookie(c, "old") # Max-Age=0; returns the value the request carried
113
+ return c.json(profile)
114
+ ```
115
+
116
+ `setCookie` adds a `Set-Cookie` header to the response the handler returns (`c.header(name, value, append=True)` underneath); `path` is `/` unless given, and the options are the header's attributes (`domain`, `expires`, `httpOnly`, `maxAge`, `path`, `secure`, `sameSite`, `priority`, `partitioned`) plus `prefix`: `"secure"` names the cookie `__Secure-<name>`, `"host"` `__Host-<name>`, each with the attributes it requires. A signed cookie carries an HMAC-SHA256 of its value, in the format Takibi (npm) uses, so the two read each other's cookies with the same secret. `generateCookie` / `generateSignedCookie` give the header value alone.
117
+
118
+ ## OpenAPI
119
+
120
+ `app.doc(path, config)` serves the document and `app.getOpenAPIDocument(config)` returns it; `config` must give `openapi` (3.0.x or 3.1.x) and `info`. Dataclasses, `TypedDict`s and `Enum`s become `components.schemas`. Swagger UI: [takibi-swagger-ui](../takibi-swagger-ui).
121
+
122
+ ## Development
123
+
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.
125
+
126
+ ## License
127
+
128
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/takibiapi/LICENSE) for more information.
@@ -0,0 +1,25 @@
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
+ )
@@ -0,0 +1,7 @@
1
+ # takibiapi
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial release
@@ -0,0 +1,20 @@
1
+ [package]
2
+ name = "takibiapi"
3
+ description = "Rust engine (takibiapi._core) behind the takibiapi Python package: request type checks, JSON, routing"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+ repository.workspace = true
8
+ publish = false
9
+ readme = "README.md"
10
+
11
+ [lib]
12
+ name = "_core"
13
+ # cdylib: the Python extension module; rlib: lets `cargo test` run pure-Rust unit tests.
14
+ crate-type = ["cdylib", "rlib"]
15
+
16
+ [dependencies]
17
+ pyo3.workspace = true
18
+
19
+ [build-dependencies]
20
+ pyo3-build-config.workspace = true
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 - present, takibiapi Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,128 @@
1
+ # TakibiAPI
2
+
3
+ REST APIs whose OpenAPI document, request checks and handler types come from Python's own types (dataclasses, `TypedDict`, `Literal`, `Enum`, `Annotated`). No runtime dependencies; JSON, routing and type checks run in a Rust extension. ASGI and Granian's RSGI.
4
+
5
+ ```sh
6
+ pip install takibiapi
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ from dataclasses import dataclass
13
+
14
+ from takibiapi import Context, TakibiAPI, createRoute
15
+
16
+
17
+ @dataclass
18
+ class Message:
19
+ message: str
20
+
21
+
22
+ class getTakibiRoute(
23
+ createRoute(
24
+ {
25
+ "method": "get",
26
+ "path": "/takibi",
27
+ "responses": {200: {"description": "takibišŸ”„", "content": {"application/json": {"schema": Message}}}},
28
+ }
29
+ )
30
+ ):
31
+ """Say takibišŸ”„."""
32
+
33
+
34
+ def getTakibiRouteHandler(c: Context[getTakibiRoute]):
35
+ return c.json(Message("takibišŸ”„"))
36
+
37
+
38
+ app = TakibiAPI()
39
+ app.openapi(getTakibiRoute, getTakibiRouteHandler)
40
+ app.doc("/doc", {"openapi": "3.1.0", "info": {"title": "takibi API", "version": "1.0.0"}})
41
+ ```
42
+
43
+ ```bash
44
+ granian --interface rsgi myapp:app # or: uvicorn myapp:app
45
+ curl localhost:8000/takibi # {"message":"takibišŸ”„"}
46
+ ```
47
+
48
+ ## Routes and handlers
49
+
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
+
52
+ ## Requests
53
+
54
+ 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")`:
55
+
56
+ ```python
57
+ params = c.req.valid("param") # "/todos/7" -> {"todoId": 7}: text converted to the declared int
58
+ ```
59
+
60
+ Only types are checked: `Annotated` keywords (`minLength`, `maximum`, ...) are written to the document but not checked, and responses are not checked at run time. For those, use [pydantic-takibiapi](../pydantic-takibiapi) or [zopy-takibiapi](../zopy-takibiapi).
61
+
62
+ A wrong type gets **422** Problem Details (`errors: [{in, path, code, message}]`), invalid JSON **400**, a required body with another Content-Type **415**.
63
+
64
+ ## Errors
65
+
66
+ ```python
67
+ app.onError(lambda c: c.json({"error": str(c.error)}, 500)) # exceptions from handlers and middleware
68
+ app.notFound(lambda c: c.text("not here", 404)) # paths with no route
69
+ ```
70
+
71
+ Both take handlers `def h(c)` (the exception is `c.error`) and middleware `async def mw(c, next)`, and registrations compose instead of replacing each other: middleware runs around the handlers registered after it (`await next()` is their response, `None` when none answered), a handler ends the chain by returning a response, and `prefix="/api"` limits a registration to those paths. The handlers of a mounted app (`app.route("/v1", v1)`) come first for its paths, then the parent's in registration order.
72
+
73
+ ```python
74
+ async def report(c, next):
75
+ res = await next()
76
+ log.warning("%s %s -> %s", c.req.method, c.req.path, res.status if res else "default")
77
+ return res
78
+
79
+
80
+ app.onError(report, lambda c: c.text("API failed", 500), prefix="/api")
81
+ app.notFound(report, lambda c: c.text("no such API", 404), prefix="/api")
82
+ ```
83
+
84
+ Without them, Takibi answers 500 / 404 Problem Details (and 405 with `Allow` for a known path), which also answer when a chain ends without a response.
85
+
86
+ ## Middleware and CORS
87
+
88
+ `app.use(middleware, prefix="/api")` runs `async def mw(c, next)` around the handlers. CORS, as Hono's `hono/cors`:
89
+
90
+ ```python
91
+ from takibiapi.cors import cors
92
+
93
+ app.use(cors()) # Access-Control-Allow-Origin: *
94
+ app.use(cors({"origin": ["https://a.example"], "credentials": True, "maxAge": 600}), prefix="/api")
95
+ ```
96
+
97
+ Options: `origin` (`"*"`, an origin, a list, or `(origin, c) -> origin | None`), `allowMethods`, `allowHeaders`, `exposeHeaders`, `maxAge`, `credentials`. Preflights (`OPTIONS`) are answered with 204.
98
+
99
+ ## Cookies
100
+
101
+ `takibiapi.cookie` reads and sets cookies, plain or signed:
102
+
103
+ ```python
104
+ from takibiapi.cookie import deleteCookie, getCookie, getSignedCookie, setCookie, setSignedCookie
105
+
106
+
107
+ def getProfileRouteHandler(c: Context[getProfileRoute]):
108
+ theme = getCookie(c, "theme") # str | None; getCookie(c) is every cookie as a dict
109
+ session = getSignedCookie(c, secret, "session") # the value, False if tampered with, None if absent
110
+ setCookie(c, "theme", "dark", {"maxAge": 86400, "httpOnly": True, "sameSite": "Lax"})
111
+ setSignedCookie(c, "session", "42", secret)
112
+ deleteCookie(c, "old") # Max-Age=0; returns the value the request carried
113
+ return c.json(profile)
114
+ ```
115
+
116
+ `setCookie` adds a `Set-Cookie` header to the response the handler returns (`c.header(name, value, append=True)` underneath); `path` is `/` unless given, and the options are the header's attributes (`domain`, `expires`, `httpOnly`, `maxAge`, `path`, `secure`, `sameSite`, `priority`, `partitioned`) plus `prefix`: `"secure"` names the cookie `__Secure-<name>`, `"host"` `__Host-<name>`, each with the attributes it requires. A signed cookie carries an HMAC-SHA256 of its value, in the format Takibi (npm) uses, so the two read each other's cookies with the same secret. `generateCookie` / `generateSignedCookie` give the header value alone.
117
+
118
+ ## OpenAPI
119
+
120
+ `app.doc(path, config)` serves the document and `app.getOpenAPIDocument(config)` returns it; `config` must give `openapi` (3.0.x or 3.1.x) and `info`. Dataclasses, `TypedDict`s and `Enum`s become `components.schemas`. Swagger UI: [takibi-swagger-ui](../takibi-swagger-ui).
121
+
122
+ ## Development
123
+
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.
125
+
126
+ ## License
127
+
128
+ Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/takibi/blob/main/pypi/takibiapi/LICENSE) for more information.