richapi 0.3.1__tar.gz → 0.4.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.
- {richapi-0.3.1 → richapi-0.4.0}/.github/workflows/main.yml +1 -1
- richapi-0.4.0/.github/workflows/on-release-main.yml +34 -0
- {richapi-0.3.1 → richapi-0.4.0}/PKG-INFO +3 -3
- richapi-0.4.0/benchmarks/bench.py +59 -0
- richapi-0.4.0/benchmarks/compare.py +82 -0
- richapi-0.4.0/benchmarks/generate_project.py +172 -0
- {richapi-0.3.1 → richapi-0.4.0}/docs/advance.md +24 -0
- {richapi-0.3.1 → richapi-0.4.0}/makefile +3 -0
- {richapi-0.3.1 → richapi-0.4.0}/pyproject.toml +4 -3
- {richapi-0.3.1 → richapi-0.4.0}/richapi/__init__.py +6 -1
- richapi-0.4.0/richapi/__version__.py +1 -0
- {richapi-0.3.1 → richapi-0.4.0}/richapi/exc_parser/compiler.py +406 -88
- {richapi-0.3.1 → richapi-0.4.0}/richapi/exc_parser/openapi.py +162 -34
- {richapi-0.3.1 → richapi-0.4.0}/richapi/exc_parser/protocol.py +41 -12
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_fastapi_auth_functions.py +3 -1
- richapi-0.4.0/tests/test_include_router.py +72 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_instance_attr_call.py +5 -5
- richapi-0.4.0/tests/test_performance.py +222 -0
- richapi-0.4.0/tests/test_raise_returned_exception.py +53 -0
- richapi-0.4.0/tests/test_recursive_func.py +57 -0
- richapi-0.4.0/tests/test_router_exception.py +57 -0
- {richapi-0.3.1 → richapi-0.4.0}/tox.ini +10 -10
- {richapi-0.3.1 → richapi-0.4.0}/uv.lock +468 -555
- richapi-0.3.1/.github/workflows/on-release-main.yml +0 -40
- richapi-0.3.1/richapi/__version__.py +0 -1
- {richapi-0.3.1 → richapi-0.4.0}/.envrc +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/.github/workflows/deploy-docs-on-demand.yml +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/.gitignore +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/README.md +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/deploy_docs.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/docs/conventions.md +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/docs/images/custom-exception.png +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/docs/images/with-richapi.png +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/docs/images/without-richapi.png +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/docs/implementation.md +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/docs/index.md +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/mkdocs.yml +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/richapi/cli.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/richapi/exc_parser/__init__.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/richapi/exc_parser/handler.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/richapi/exceptions.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/__init__.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/app/__init__.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/app/foo1.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_builder_pattern.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_class.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_compiler.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_custom_exc.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_duplicated_function.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_exc_raising_cases.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_exception_edge_cases.py +0 -0
- {richapi-0.3.1 → richapi-0.4.0}/tests/test_import_from_other_module.py +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: release-main
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Check out
|
|
12
|
+
uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Set up uv
|
|
15
|
+
uses: astral-sh/setup-uv@v6
|
|
16
|
+
|
|
17
|
+
- name: Check version matches the release tag
|
|
18
|
+
run: |
|
|
19
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
20
|
+
pkg="$(uv version --short)"
|
|
21
|
+
init="$(sed -n 's/^__version__ = "\(.*\)"/\1/p' richapi/__version__.py)"
|
|
22
|
+
if [ "$tag" != "$pkg" ] || [ "$tag" != "$init" ]; then
|
|
23
|
+
echo "::error::Release tag is $tag but pyproject.toml has $pkg and __version__.py has $init. Bump both before releasing."
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# rye publish bundles an old twine that rejects current hatchling metadata (2.4+).
|
|
28
|
+
- name: Build
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Publish
|
|
32
|
+
run: uv publish --token "$PYPI_TOKEN"
|
|
33
|
+
env:
|
|
34
|
+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: richapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Find HTTPExceptions and turn them into documented responses!
|
|
5
5
|
Project-URL: repository, https://github.com/ManiMozaffar/richapi
|
|
6
6
|
Project-URL: Homepage, https://github.com/ManiMozaffar/richapi
|
|
7
7
|
Project-URL: Documentation, https://github.com/ManiMozaffar/richapi
|
|
8
8
|
Project-URL: Source, https://github.com/ManiMozaffar/richapi
|
|
9
9
|
License-Expression: MIT
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: fastapi>=0.105
|
|
12
12
|
Requires-Dist: typer>=0.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Benchmark richapi on a generated project.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
uv run python benchmarks/bench.py [--size small|medium|large] [--out schema.json]
|
|
5
|
+
|
|
6
|
+
Runs in a fresh process each time so no state leaks between runs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import importlib
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
19
|
+
from generate_project import generate # noqa: E402
|
|
20
|
+
|
|
21
|
+
SIZES: dict[str, dict[str, Any]] = {
|
|
22
|
+
"small": dict(n_services=5, n_funcs=20, n_routers=4, n_endpoints=5),
|
|
23
|
+
"medium": dict(n_services=30, n_funcs=40, n_routers=20, n_endpoints=15),
|
|
24
|
+
"large": dict(n_services=80, n_funcs=80, n_routers=60, n_endpoints=20),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
parser = argparse.ArgumentParser()
|
|
30
|
+
parser.add_argument("--size", choices=SIZES, default="medium")
|
|
31
|
+
parser.add_argument("--out", type=Path, default=None)
|
|
32
|
+
parser.add_argument("--repeat", type=int, default=1)
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
|
|
35
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
36
|
+
pkg = generate(Path(tmp), **SIZES[args.size])
|
|
37
|
+
sys.path.insert(0, tmp)
|
|
38
|
+
app = importlib.import_module(f"{pkg}.main").app
|
|
39
|
+
|
|
40
|
+
from richapi.exc_parser.openapi import compile_openapi_from_fastapi
|
|
41
|
+
|
|
42
|
+
n_routes = len(app.routes)
|
|
43
|
+
timings = []
|
|
44
|
+
schema = None
|
|
45
|
+
for _ in range(args.repeat):
|
|
46
|
+
start = time.perf_counter()
|
|
47
|
+
schema = compile_openapi_from_fastapi(app, pkg)
|
|
48
|
+
timings.append(time.perf_counter() - start)
|
|
49
|
+
|
|
50
|
+
print(
|
|
51
|
+
f"size={args.size} routes={n_routes} "
|
|
52
|
+
f"best={min(timings):.3f}s runs={[round(t, 3) for t in timings]}"
|
|
53
|
+
)
|
|
54
|
+
if args.out:
|
|
55
|
+
args.out.write_text(json.dumps(schema, indent=2, sort_keys=True))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Compare richapi from a git ref (default: main) against the working tree.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
uv run python benchmarks/compare.py [--ref main] [--size medium]
|
|
5
|
+
|
|
6
|
+
Checks out <ref> into a temporary git worktree, runs bench.py with each
|
|
7
|
+
version on the same generated project, and verifies both produce the
|
|
8
|
+
exact same OpenAPI schema.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
19
|
+
BENCH = Path(__file__).resolve().parent / "bench.py"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(richapi_root: Path, size: str, out: Path, repeat: int) -> str:
|
|
23
|
+
env = {**os.environ, "PYTHONPATH": str(richapi_root)}
|
|
24
|
+
cmd = [sys.executable, str(BENCH), "--size", size, "--out", str(out)]
|
|
25
|
+
cmd += ["--repeat", str(repeat)]
|
|
26
|
+
return subprocess.run(
|
|
27
|
+
cmd, env=env, check=True, capture_output=True, text=True
|
|
28
|
+
).stdout.strip()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main() -> None:
|
|
32
|
+
parser = argparse.ArgumentParser()
|
|
33
|
+
parser.add_argument("--ref", default="main")
|
|
34
|
+
parser.add_argument("--size", default="medium")
|
|
35
|
+
parser.add_argument("--repeat", type=int, default=1)
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
39
|
+
tmp = Path(tmp)
|
|
40
|
+
worktree = tmp / "ref"
|
|
41
|
+
subprocess.run(
|
|
42
|
+
[
|
|
43
|
+
"git",
|
|
44
|
+
"-C",
|
|
45
|
+
str(ROOT),
|
|
46
|
+
"worktree",
|
|
47
|
+
"add",
|
|
48
|
+
"-q",
|
|
49
|
+
"--detach",
|
|
50
|
+
str(worktree),
|
|
51
|
+
args.ref,
|
|
52
|
+
],
|
|
53
|
+
check=True,
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
print(
|
|
57
|
+
f"{args.ref:>10}: {run(worktree, args.size, tmp / 'ref.json', args.repeat)}"
|
|
58
|
+
)
|
|
59
|
+
print(
|
|
60
|
+
f"{'current':>10}: {run(ROOT, args.size, tmp / 'cur.json', args.repeat)}"
|
|
61
|
+
)
|
|
62
|
+
same = (tmp / "ref.json").read_text() == (tmp / "cur.json").read_text()
|
|
63
|
+
print("schemas identical" if same else "SCHEMAS DIFFER")
|
|
64
|
+
finally:
|
|
65
|
+
subprocess.run(
|
|
66
|
+
[
|
|
67
|
+
"git",
|
|
68
|
+
"-C",
|
|
69
|
+
str(ROOT),
|
|
70
|
+
"worktree",
|
|
71
|
+
"remove",
|
|
72
|
+
"--force",
|
|
73
|
+
str(worktree),
|
|
74
|
+
],
|
|
75
|
+
check=True,
|
|
76
|
+
)
|
|
77
|
+
if not same:
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Generate a synthetic, large FastAPI project to benchmark richapi's analysis.
|
|
2
|
+
|
|
3
|
+
The layout mimics a real service: exceptions, many service modules full of
|
|
4
|
+
helpers/classes calling each other, shared dependencies and routers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import random
|
|
8
|
+
import textwrap
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _w(path: Path, body: str) -> None:
|
|
13
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
path.write_text(textwrap.dedent(body).lstrip())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate(
|
|
18
|
+
root: Path,
|
|
19
|
+
pkg: str = "bench_app",
|
|
20
|
+
n_services: int = 30,
|
|
21
|
+
n_funcs: int = 40,
|
|
22
|
+
n_routers: int = 20,
|
|
23
|
+
n_endpoints: int = 15,
|
|
24
|
+
seed: int = 0,
|
|
25
|
+
) -> str:
|
|
26
|
+
rnd = random.Random(seed)
|
|
27
|
+
base = root / pkg
|
|
28
|
+
_w(base / "__init__.py", "")
|
|
29
|
+
_w(base / "services" / "__init__.py", "")
|
|
30
|
+
_w(base / "routes" / "__init__.py", "")
|
|
31
|
+
|
|
32
|
+
exc_lines = [
|
|
33
|
+
"from fastapi import HTTPException",
|
|
34
|
+
"from pydantic.dataclasses import dataclass",
|
|
35
|
+
"from richapi import BaseHTTPException, RichHTTPException",
|
|
36
|
+
"",
|
|
37
|
+
]
|
|
38
|
+
for i in range(40):
|
|
39
|
+
exc_lines += [
|
|
40
|
+
"@dataclass",
|
|
41
|
+
f"class Rich{i}(RichHTTPException):",
|
|
42
|
+
f' """Rich error {i}"""',
|
|
43
|
+
f" status_code = {400 + i % 30}",
|
|
44
|
+
f" field_{i}: int = 0",
|
|
45
|
+
"",
|
|
46
|
+
f"class Plain{i}(HTTPException): ...",
|
|
47
|
+
"",
|
|
48
|
+
f"class Base{i}(BaseHTTPException):",
|
|
49
|
+
f" status_code = {400 + (i * 7) % 30}",
|
|
50
|
+
f' detail = "base error {i}"',
|
|
51
|
+
"",
|
|
52
|
+
]
|
|
53
|
+
_w(base / "exceptions.py", "\n".join(exc_lines) + "\n")
|
|
54
|
+
|
|
55
|
+
for s in range(n_services):
|
|
56
|
+
lines = [
|
|
57
|
+
"from fastapi import HTTPException, status",
|
|
58
|
+
f"from {pkg} import exceptions as E",
|
|
59
|
+
f"from {pkg}.exceptions import Rich{s % 40}, Plain{s % 40}",
|
|
60
|
+
]
|
|
61
|
+
if s > 0:
|
|
62
|
+
lines.append(f"from {pkg}.services import svc_{s - 1} as prev")
|
|
63
|
+
lines.append("")
|
|
64
|
+
for f in range(n_funcs):
|
|
65
|
+
kind = rnd.randrange(5)
|
|
66
|
+
lines.append(f"def helper_{f}(x: int = 0):")
|
|
67
|
+
if kind == 0:
|
|
68
|
+
lines.append(f" raise Rich{s % 40}()")
|
|
69
|
+
elif kind == 1:
|
|
70
|
+
lines.append(
|
|
71
|
+
f' raise Plain{s % 40}(status_code={400 + f % 30}, detail="svc {s} helper {f}")'
|
|
72
|
+
)
|
|
73
|
+
elif kind == 2:
|
|
74
|
+
lines.append(f" exc = E.Base{(s + f) % 40}()")
|
|
75
|
+
lines.append(" raise exc")
|
|
76
|
+
elif kind == 3 and f > 0:
|
|
77
|
+
lines.append(f" helper_{rnd.randrange(f)}(x)")
|
|
78
|
+
lines.append(
|
|
79
|
+
' raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="missing")'
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
lines.append(" total = 0")
|
|
83
|
+
lines.append(" for i in range(x):")
|
|
84
|
+
lines.append(" total += i")
|
|
85
|
+
lines.append(" return total")
|
|
86
|
+
if s > 0 and rnd.random() < 0.3:
|
|
87
|
+
lines.append(f" prev.helper_{rnd.randrange(n_funcs)}(x)")
|
|
88
|
+
lines.append("")
|
|
89
|
+
# filler to make modules realistically large
|
|
90
|
+
for f in range(n_funcs):
|
|
91
|
+
lines += [
|
|
92
|
+
f"def _private_{f}(a, b, c=None):",
|
|
93
|
+
' """Some unrelated code that richapi has to parse anyway."""',
|
|
94
|
+
" data = {'a': a, 'b': b, 'c': c}",
|
|
95
|
+
" return [v for v in data.values() if v is not None]",
|
|
96
|
+
"",
|
|
97
|
+
]
|
|
98
|
+
lines += [
|
|
99
|
+
f"class Repo{s}:",
|
|
100
|
+
" def get(self, x):",
|
|
101
|
+
f" helper_{rnd.randrange(n_funcs)}(x)",
|
|
102
|
+
" return x",
|
|
103
|
+
"",
|
|
104
|
+
f"class Service{s}:",
|
|
105
|
+
f" repo: Repo{s}",
|
|
106
|
+
"",
|
|
107
|
+
" def __init__(self):",
|
|
108
|
+
f" self.repo = Repo{s}()",
|
|
109
|
+
f" helper_{rnd.randrange(n_funcs)}()",
|
|
110
|
+
"",
|
|
111
|
+
]
|
|
112
|
+
for m in range(10):
|
|
113
|
+
lines += [
|
|
114
|
+
f" def method_{m}(self, x: int):",
|
|
115
|
+
f" helper_{rnd.randrange(n_funcs)}(x)",
|
|
116
|
+
" self.repo.get(x)",
|
|
117
|
+
" return self",
|
|
118
|
+
"",
|
|
119
|
+
]
|
|
120
|
+
_w(base / "services" / f"svc_{s}.py", "\n".join(lines) + "\n")
|
|
121
|
+
|
|
122
|
+
_w(
|
|
123
|
+
base / "deps.py",
|
|
124
|
+
f"""
|
|
125
|
+
import fastapi
|
|
126
|
+
import fastapi.security
|
|
127
|
+
from {pkg}.services import svc_0
|
|
128
|
+
|
|
129
|
+
bearer = fastapi.security.HTTPBearer()
|
|
130
|
+
|
|
131
|
+
def get_token(token=fastapi.Depends(bearer)):
|
|
132
|
+
svc_0.helper_0()
|
|
133
|
+
return token
|
|
134
|
+
|
|
135
|
+
def get_user(token: str = fastapi.Depends(get_token)):
|
|
136
|
+
if not token:
|
|
137
|
+
raise fastapi.HTTPException(status_code=401, detail="unauthorized")
|
|
138
|
+
return token
|
|
139
|
+
""",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
for r in range(n_routers):
|
|
143
|
+
lines = [
|
|
144
|
+
"from fastapi import APIRouter, Depends, HTTPException",
|
|
145
|
+
f"from {pkg}.deps import get_user",
|
|
146
|
+
]
|
|
147
|
+
used = sorted({rnd.randrange(n_services) for _ in range(4)})
|
|
148
|
+
for s in used:
|
|
149
|
+
lines.append(f"from {pkg}.services import svc_{s}")
|
|
150
|
+
lines.append(f"from {pkg}.services.svc_{s} import Service{s}")
|
|
151
|
+
lines += ["", "router = APIRouter()", ""]
|
|
152
|
+
for e in range(n_endpoints):
|
|
153
|
+
s = rnd.choice(used)
|
|
154
|
+
lines += [
|
|
155
|
+
f'@router.get("/r{r}/e{e}")',
|
|
156
|
+
f"async def endpoint_{r}_{e}(x: int, user: str = Depends(get_user)):",
|
|
157
|
+
f" svc = Service{s}()",
|
|
158
|
+
f" svc.method_{rnd.randrange(10)}(x).method_{rnd.randrange(10)}(x)",
|
|
159
|
+
f" svc_{s}.helper_{rnd.randrange(n_funcs)}(x)",
|
|
160
|
+
" if x < 0:",
|
|
161
|
+
f' raise HTTPException(status_code={400 + e % 20}, detail="route {r} {e}")',
|
|
162
|
+
" return {'ok': True}",
|
|
163
|
+
"",
|
|
164
|
+
]
|
|
165
|
+
_w(base / "routes" / f"r_{r}.py", "\n".join(lines) + "\n")
|
|
166
|
+
|
|
167
|
+
lines = ["from fastapi import FastAPI"]
|
|
168
|
+
lines += [f"from {pkg}.routes import r_{r}" for r in range(n_routers)]
|
|
169
|
+
lines += ["", "app = FastAPI()"]
|
|
170
|
+
lines += [f"app.include_router(r_{r}.router)" for r in range(n_routers)]
|
|
171
|
+
_w(base / "main.py", "\n".join(lines) + "\n")
|
|
172
|
+
return pkg
|
|
@@ -49,3 +49,27 @@ app = FastAPI()
|
|
|
49
49
|
# add your routers ....
|
|
50
50
|
app.openapi = enrich_openapi(app, ["your_module_name", "some-other-package"])
|
|
51
51
|
```
|
|
52
|
+
|
|
53
|
+
### Finding exceptions in specific API functions of FastAPI
|
|
54
|
+
|
|
55
|
+
If you want to limit the search of exceptions to specific functions, you can pass the list of functions to the `find_exceptions_in_api_functions` function.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from richapi import find_exceptions_in_api_functions, BaseHTTPException
|
|
59
|
+
from fastapi import FastAPI
|
|
60
|
+
|
|
61
|
+
app = FastAPI()
|
|
62
|
+
|
|
63
|
+
class NotFoundException(BaseHTTPException):
|
|
64
|
+
status_code = 404
|
|
65
|
+
detail: str = "Item not found"
|
|
66
|
+
|
|
67
|
+
@app.get("/items/{item_id}")
|
|
68
|
+
async def read_item(item_id: int):
|
|
69
|
+
if item_id != 42:
|
|
70
|
+
raise NotFoundException()
|
|
71
|
+
return {"item_id": item_id}
|
|
72
|
+
|
|
73
|
+
exceptions = find_exceptions_in_api_functions(funcs=[read_item], fastapi_app=app)
|
|
74
|
+
print(exceptions) # {NotFoundException}
|
|
75
|
+
```
|
|
@@ -14,6 +14,9 @@ test: ## Test the code with pytest
|
|
|
14
14
|
@echo "🚀 Testing code: Running pytest"
|
|
15
15
|
uv run pytest
|
|
16
16
|
|
|
17
|
+
.PHONY: bench
|
|
18
|
+
bench: ## Benchmark analysis speed against main on a generated project
|
|
19
|
+
uv run python benchmarks/compare.py --ref main --size medium
|
|
17
20
|
|
|
18
21
|
.PHONY: docs
|
|
19
22
|
docs: ## Build and serve the documentation
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "richapi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Find HTTPExceptions and turn them into documented responses!"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
7
|
dependencies = ["typer>=0.12", "fastapi>=0.105"]
|
|
8
8
|
license = 'MIT'
|
|
9
9
|
|
|
@@ -19,6 +19,7 @@ build-backend = "hatchling.build"
|
|
|
19
19
|
|
|
20
20
|
[tool.pytest.ini_options]
|
|
21
21
|
asyncio_default_fixture_loop_scope = "session"
|
|
22
|
+
addopts = "--log-level=DEBUG -v"
|
|
22
23
|
|
|
23
24
|
[tool.pyright]
|
|
24
25
|
typeCheckingMode = "standard"
|
|
@@ -35,7 +36,7 @@ packages = ["richapi"]
|
|
|
35
36
|
[tool.uv]
|
|
36
37
|
dev-dependencies = [
|
|
37
38
|
"ruff>=0.6.9",
|
|
38
|
-
"pyright>=1.1.
|
|
39
|
+
"pyright>=1.1.397",
|
|
39
40
|
"rich>=13.9.2",
|
|
40
41
|
"pytest>=8.3.3",
|
|
41
42
|
"pytest-asyncio>=0.24.0",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from richapi.exc_parser.handler import add_exc_handler
|
|
2
|
-
from richapi.exc_parser.openapi import
|
|
2
|
+
from richapi.exc_parser.openapi import (
|
|
3
|
+
enrich_openapi,
|
|
4
|
+
find_exceptions_in_api_functions,
|
|
5
|
+
load_openapi,
|
|
6
|
+
)
|
|
3
7
|
from richapi.exc_parser.protocol import BaseHTTPException, RichHTTPException
|
|
4
8
|
|
|
5
9
|
__all__ = [
|
|
@@ -8,4 +12,5 @@ __all__ = [
|
|
|
8
12
|
"BaseHTTPException",
|
|
9
13
|
"RichHTTPException",
|
|
10
14
|
"add_exc_handler",
|
|
15
|
+
"find_exceptions_in_api_functions",
|
|
11
16
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|