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.
- takibiapi-0.1.0/Cargo.lock +133 -0
- takibiapi-0.1.0/Cargo.toml +23 -0
- takibiapi-0.1.0/LICENSE +21 -0
- takibiapi-0.1.0/PKG-INFO +152 -0
- takibiapi-0.1.0/README.md +128 -0
- takibiapi-0.1.0/pypi/takibiapi/BUILD.bazel +25 -0
- takibiapi-0.1.0/pypi/takibiapi/CHANGELOG.md +7 -0
- takibiapi-0.1.0/pypi/takibiapi/Cargo.toml +20 -0
- takibiapi-0.1.0/pypi/takibiapi/LICENSE +21 -0
- takibiapi-0.1.0/pypi/takibiapi/README.md +128 -0
- takibiapi-0.1.0/pypi/takibiapi/build.rs +16 -0
- takibiapi-0.1.0/pypi/takibiapi/python/BUILD.bazel +29 -0
- takibiapi-0.1.0/pypi/takibiapi/src/encode.rs +236 -0
- takibiapi-0.1.0/pypi/takibiapi/src/engine.rs +647 -0
- takibiapi-0.1.0/pypi/takibiapi/src/env.rs +235 -0
- takibiapi-0.1.0/pypi/takibiapi/src/json.rs +368 -0
- takibiapi-0.1.0/pypi/takibiapi/src/lib.rs +192 -0
- takibiapi-0.1.0/pypi/takibiapi/src/plan.rs +255 -0
- takibiapi-0.1.0/pypi/takibiapi/src/router.rs +162 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/test_app.py +586 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/test_cookie.py +198 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/test_cors.py +135 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/test_json.py +297 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/test_router.py +91 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/test_schema.py +292 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/test_typing.py +28 -0
- takibiapi-0.1.0/pypi/takibiapi/tests/typing/routes.py +97 -0
- takibiapi-0.1.0/pyproject.toml +37 -0
- takibiapi-0.1.0/python/takibiapi/__init__.py +41 -0
- takibiapi-0.1.0/python/takibiapi/_core.pyi +22 -0
- takibiapi-0.1.0/python/takibiapi/app.py +451 -0
- takibiapi-0.1.0/python/takibiapi/context.py +317 -0
- takibiapi-0.1.0/python/takibiapi/cookie.py +282 -0
- takibiapi-0.1.0/python/takibiapi/cors.py +112 -0
- takibiapi-0.1.0/python/takibiapi/openapi.py +98 -0
- takibiapi-0.1.0/python/takibiapi/problems.py +34 -0
- takibiapi-0.1.0/python/takibiapi/py.typed +0 -0
- takibiapi-0.1.0/python/takibiapi/route.py +408 -0
- takibiapi-0.1.0/python/takibiapi/router.py +115 -0
- takibiapi-0.1.0/python/takibiapi/schema.py +582 -0
- 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
|
takibiapi-0.1.0/LICENSE
ADDED
|
@@ -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.
|
takibiapi-0.1.0/PKG-INFO
ADDED
|
@@ -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,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.
|