richapi 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.
- {richapi-0.1.0 → richapi-0.2.0}/.gitignore +1 -0
- {richapi-0.1.0 → richapi-0.2.0}/PKG-INFO +4 -3
- {richapi-0.1.0 → richapi-0.2.0}/README.md +1 -0
- {richapi-0.1.0 → richapi-0.2.0}/docs/advance.md +18 -0
- {richapi-0.1.0 → richapi-0.2.0}/docs/conventions.md +24 -5
- richapi-0.2.0/docs/implementation.md +55 -0
- {richapi-0.1.0 → richapi-0.2.0}/docs/index.md +2 -2
- {richapi-0.1.0 → richapi-0.2.0}/mkdocs.yml +1 -0
- {richapi-0.1.0 → richapi-0.2.0}/pyproject.toml +2 -3
- richapi-0.2.0/richapi/__version__.py +1 -0
- {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/compiler.py +101 -23
- {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/openapi.py +9 -2
- {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/protocol.py +13 -0
- {richapi-0.1.0 → richapi-0.2.0}/tests/test_exc_raising_cases.py +1 -15
- richapi-0.2.0/tests/test_fastapi_auth_functions.py +30 -0
- {richapi-0.1.0 → richapi-0.2.0}/uv.lock +3 -27
- richapi-0.1.0/.python-version +0 -1
- richapi-0.1.0/richapi/__version__.py +0 -1
- {richapi-0.1.0 → richapi-0.2.0}/.envrc +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/.github/workflows/deploy-docs-on-demand.yml +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/.github/workflows/main.yml +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/.github/workflows/on-release-main.yml +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/deploy_docs.py +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/docs/images/with-richapi.png +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/docs/images/without-richapi.png +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/makefile +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/richapi/__init__.py +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/richapi/cli.py +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/__init__.py +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/richapi/exceptions.py +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/tests/__init__.py +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/tests/test_exception_edge_cases.py +0 -0
- {richapi-0.1.0 → richapi-0.2.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: richapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -8,8 +8,8 @@ Project-URL: Documentation, https://github.com/ManiMozaffar/richapi
|
|
|
8
8
|
Project-URL: Source, https://github.com/ManiMozaffar/richapi
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
|
-
Requires-Dist:
|
|
12
|
-
Requires-Dist: typer>=0.12
|
|
11
|
+
Requires-Dist: fastapi>=0.105
|
|
12
|
+
Requires-Dist: typer>=0.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
15
15
|
### Installation
|
|
@@ -28,6 +28,7 @@ See [documentation](https://manimozaffar.github.io/richapi/latest/) for more det
|
|
|
28
28
|
|
|
29
29
|
RichAPI is designed to simplify FastAPI projects by automatically generating documented HTTP responses from HTTPException without extra effort! 🎉
|
|
30
30
|
|
|
31
|
+
This project is inspired and completer version of [Kludex's Fast-Response project](https://github.com/Kludex/fastapi-responses)
|
|
31
32
|
This package is planned to have more features for now that might be essential for any FastAPI project later.
|
|
32
33
|
|
|
33
34
|
What would you see WITHOUT RichAPI:
|
|
@@ -14,6 +14,7 @@ See [documentation](https://manimozaffar.github.io/richapi/latest/) for more det
|
|
|
14
14
|
|
|
15
15
|
RichAPI is designed to simplify FastAPI projects by automatically generating documented HTTP responses from HTTPException without extra effort! 🎉
|
|
16
16
|
|
|
17
|
+
This project is inspired and completer version of [Kludex's Fast-Response project](https://github.com/Kludex/fastapi-responses)
|
|
17
18
|
This package is planned to have more features for now that might be essential for any FastAPI project later.
|
|
18
19
|
|
|
19
20
|
What would you see WITHOUT RichAPI:
|
|
@@ -34,3 +34,21 @@ class YourException(BaseHTTPException):
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Remember that the responsbility of raising and returning the correct data on FastAPI is not library responsibility, but the responsibility of the developer. The library is only responsible for generating the OpenAPI schema by looking at the functions and classes that are raising exceptions.
|
|
37
|
+
|
|
38
|
+
### Finding exceptions in other modules
|
|
39
|
+
|
|
40
|
+
By default, the library only looks at the FastAPI app module to search for exceptions.
|
|
41
|
+
This is to avoid parsing countless dependency modules that are not related to the FastAPI app.
|
|
42
|
+
But sometimes you may be using some other libraries that raises HTTPExceptions.
|
|
43
|
+
For example using a third library for JWT authentication.
|
|
44
|
+
|
|
45
|
+
In that case, you can simply pass the module name to the `parse_exceptions` function.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from richapi import enrich_openapi
|
|
49
|
+
from fastapi import FastAPI
|
|
50
|
+
|
|
51
|
+
app = FastAPI()
|
|
52
|
+
# add your routers ....
|
|
53
|
+
app.openapi = enrich_openapi(app, ["your_module_name", "some-other-package"])
|
|
54
|
+
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# Conventions
|
|
2
2
|
|
|
3
3
|
This is guideline for how to raise exceptions so that the library can generate the OpenAPI schema from them.
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ Since the library is a mini compiler using Abstract Syntax Tree and functions gl
|
|
|
6
6
|
|
|
7
7
|
Keep in mind that if you don't follow these conventions, there would be alot false positive and false negative cases, and the library may not be able to extract the exceptions and their responses correctly.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Inherit from `richapi.BaseHTTPException`
|
|
10
10
|
|
|
11
11
|
Exceptions are better to be very concrete, without having any dynamic data in them.
|
|
12
12
|
So that using `raise Class` is enough to raise them.
|
|
@@ -23,7 +23,11 @@ class UserNotFound(BaseHTTPException):
|
|
|
23
23
|
|
|
24
24
|
And then, only writing `raise UserNotFound` is enough to raise the exception. This is most ideal case.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
!!! info "Expected Behavior"
|
|
27
|
+
|
|
28
|
+
Library **MIGHT skip** adding the exception to the OpenAPI schema if it could not gather necessary and missing informations from the abstract syntax tree.
|
|
29
|
+
|
|
30
|
+
## Avoiding dynamic data in the constructor of the exceptions and stringifying errors
|
|
27
31
|
|
|
28
32
|
Exceptions should not have any dynamic data in them.
|
|
29
33
|
For example the code below is NOT ideal:
|
|
@@ -35,9 +39,16 @@ error_message = f"User {user_id} is not allowed to perform this action"
|
|
|
35
39
|
raise FooException(detail=error_message)
|
|
36
40
|
```
|
|
37
41
|
|
|
42
|
+
This is not even a good practice to begin with, because ideally your RestAPI should not be aware of what text shown to the user. The error message should be handled by the client, not the server. There is a better way you can do to pass information to client about the error.
|
|
43
|
+
Read the [advance usage](advance.md) section for more information.
|
|
44
|
+
|
|
38
45
|
Since the library is using static analyzers to extract the exceptions, it is not possible to extract the dynamic data from the exceptions. (or rather very hard to do so)
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
!!! info "Expected Behavior"
|
|
48
|
+
|
|
49
|
+
Library **WILL add** the exception to the OpenAPI schema, but the detail of response won't be precisely shown and instead you will see `str` as the response type. However, if you follow this convention, library exactly show the string that you have written in the `detail` attribute of the exception as a **literal string type**.
|
|
50
|
+
|
|
51
|
+
## Avoiding usage of magic methods
|
|
41
52
|
|
|
42
53
|
Magic methods are implicit, therefore extracting them from the abstract syntax tree is exteremly hard and equal to rewriting the Python interpreter.
|
|
43
54
|
|
|
@@ -52,7 +63,11 @@ class Foo:
|
|
|
52
63
|
raise HTTPException(status_code=400, detail="This is not allowed")
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
!!! info "Expected Behavior"
|
|
67
|
+
|
|
68
|
+
Library **WILL skip** adding the exception to the OpenAPI schema.
|
|
69
|
+
|
|
70
|
+
## Keep it simple
|
|
56
71
|
|
|
57
72
|
The ideal way to raise exceptions is to keep it simple from syntax point of view;
|
|
58
73
|
|
|
@@ -70,3 +85,7 @@ raise exc()(status_code=400, detail="This is not allowed")
|
|
|
70
85
|
```
|
|
71
86
|
|
|
72
87
|
Keep it simple, keep the exception type next to raise keyword; without any dynamic data in the constructor. This help the library to extract the exceptions and their responses and guarantee that they are correctly compiled into the OpenAPI schema.
|
|
88
|
+
|
|
89
|
+
!!! info "Expected Behavior"
|
|
90
|
+
|
|
91
|
+
Library **MIGHT skip** adding the exception to the OpenAPI schema depending on how complicated the exception raising is.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Implementation detail
|
|
2
|
+
|
|
3
|
+
Library is made of few components working together to generate the OpenAPI schema.
|
|
4
|
+
|
|
5
|
+
## Exception Finder
|
|
6
|
+
|
|
7
|
+
- **Input**: A function
|
|
8
|
+
- **Output**: List of exception type with their `raise` abstract syntax tree nodes
|
|
9
|
+
|
|
10
|
+
1. Check if this function was ever analyzed before, if yes, then skip.
|
|
11
|
+
2. Get source code of the callable object.
|
|
12
|
+
3. Parse the source code to AST.
|
|
13
|
+
4. Start visiting every node in the callable.
|
|
14
|
+
5. Track assigmnets to a variable.
|
|
15
|
+
6. On visiting a node ...
|
|
16
|
+
1. If the node is a `raise` statement, then extract the exception type and return as output.
|
|
17
|
+
2. If a node is a `call` statement, then recursively visit the callable that is being called.
|
|
18
|
+
3. If a node is an `assign` statement, then track the variable name and the assigned value.
|
|
19
|
+
4. If a node is an `attribute` statement, then track the attribute name and the parent object. Might have to recursively visit the parent object.
|
|
20
|
+
|
|
21
|
+
Note that the only way to extract the exception type is to use the tracked assigmnets with `eval` function so that the type is returned.
|
|
22
|
+
That's why it's best to run the library on CLI rather than on the fly. To understand this better look at below code;
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
|
|
26
|
+
def foo():
|
|
27
|
+
err = Exception
|
|
28
|
+
raise err
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The only way that you can understand err is Exception is to keep tracking the assignments and then use `eval` to get the type. Running `eval("err")` will get you the type of the error which is a requirement to function properly.
|
|
32
|
+
|
|
33
|
+
## FastAPI Excption Parser
|
|
34
|
+
|
|
35
|
+
- **Input**: An excpetion with its `raise` AST node
|
|
36
|
+
- **Output**: Json schema
|
|
37
|
+
|
|
38
|
+
1. Check if the exception type already has a schema defined by itself.
|
|
39
|
+
- If yes, then simply construct the Json schema from that.
|
|
40
|
+
- If not, continue to the next step.
|
|
41
|
+
2. Analyze and extract arg and kwargs from the `raise` AST node.
|
|
42
|
+
3. Try to construct the exception schema from the arguments.
|
|
43
|
+
4. Try to dynamically generate the schema using Pydantic.
|
|
44
|
+
5. Return the schema if successfully generated, otherwise return None.
|
|
45
|
+
|
|
46
|
+
## OpenAPI Generator
|
|
47
|
+
|
|
48
|
+
- **Input**: FastAPI Module, Previous OpenAPI schema
|
|
49
|
+
- **Output**: OpenAPI schema
|
|
50
|
+
|
|
51
|
+
1. Iterating over all fastapi routers to analyze each at a time.
|
|
52
|
+
2. Each router has few callable dependency. All callable dependencies are analyzed using `Exception Finder`.
|
|
53
|
+
3. Result of the `Exception Finder` is passed to the `FastAPI Excption Parser` to get the Json schema.
|
|
54
|
+
4. Using the Json schema, the OpenAPI schema is extended with new responses for that router.
|
|
55
|
+
5. The result is cached so that the same router is not analyzed again.
|
|
@@ -8,8 +8,8 @@ pip install richapi
|
|
|
8
8
|
|
|
9
9
|
### Usage
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
RichAPI is designed to simplify FastAPI projects by automatically generating documented HTTP responses from HTTPException without extra effort! 🎉
|
|
12
|
+
|
|
13
13
|
This package is planned to have more features for now that might be essential for any FastAPI project later.
|
|
14
14
|
|
|
15
15
|
There are multiple way you can do that,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "richapi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Find HTTPExceptions and turn them into documented responses!"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
7
|
-
dependencies = ["
|
|
7
|
+
dependencies = ["typer>=0.12", "fastapi>=0.105"]
|
|
8
8
|
license = 'MIT'
|
|
9
9
|
|
|
10
10
|
[project.urls]
|
|
@@ -44,7 +44,6 @@ dev-dependencies = [
|
|
|
44
44
|
"mkdocstrings[python]>=0.25.1",
|
|
45
45
|
"mkdocs-simple-hooks>=0.1.5",
|
|
46
46
|
"mkdocs-material-extensions>=1.3.1",
|
|
47
|
-
"fastapi>=0.115.0",
|
|
48
47
|
"mdx-truly-sane-lists>=1.3",
|
|
49
48
|
"mike==2.0.0",
|
|
50
49
|
"psutil>=6.0.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -60,7 +60,7 @@ def is_in_module(module: str, target_modules: list[str]) -> bool:
|
|
|
60
60
|
|
|
61
61
|
def find_explicit_exceptions(
|
|
62
62
|
func: Callable,
|
|
63
|
-
target_modules:
|
|
63
|
+
target_modules: list[str],
|
|
64
64
|
should_search_module_pred: Union[Callable[[str], bool], None] = None,
|
|
65
65
|
) -> List[Tuple[Optional[type[Exception]], ast.Raise]]:
|
|
66
66
|
"""
|
|
@@ -73,9 +73,6 @@ def find_explicit_exceptions(
|
|
|
73
73
|
- The ast.Raise node
|
|
74
74
|
"""
|
|
75
75
|
|
|
76
|
-
if isinstance(target_modules, str):
|
|
77
|
-
target_modules = [target_modules]
|
|
78
|
-
|
|
79
76
|
if should_search_module_pred is None:
|
|
80
77
|
|
|
81
78
|
def should_search_module_pred_func(target):
|
|
@@ -83,21 +80,57 @@ def find_explicit_exceptions(
|
|
|
83
80
|
|
|
84
81
|
should_search_module_pred = should_search_module_pred_func
|
|
85
82
|
|
|
83
|
+
return _find_explicit_expection_recursively(func, should_search_module_pred)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_func_name(func: Callable) -> str:
|
|
87
|
+
return func.__name__ if hasattr(func, "__name__") else func.__str__()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _find_all_class_exceptions(
|
|
91
|
+
cls: type,
|
|
92
|
+
to_filter_predicate: Callable[[str], bool],
|
|
93
|
+
tree: Optional[ast.AST] = None,
|
|
94
|
+
):
|
|
86
95
|
try:
|
|
87
|
-
|
|
88
|
-
except
|
|
89
|
-
|
|
96
|
+
tree = tree or ast.parse(inspect.getsource(cls))
|
|
97
|
+
except Exception as error:
|
|
98
|
+
logger.exception(
|
|
99
|
+
f"Failed to parse source code for {cls.__name__}.", exc_info=error
|
|
100
|
+
)
|
|
101
|
+
return []
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
result = _find_explicit_expection_recursively(
|
|
104
|
+
cls.__init__, to_filter_predicate, tree
|
|
105
|
+
)
|
|
106
|
+
if hasattr(cls, "__call__"):
|
|
107
|
+
# Not sure if __call__ was called or init, so check for both
|
|
108
|
+
|
|
109
|
+
result.extend(
|
|
110
|
+
_find_explicit_expection_recursively(
|
|
111
|
+
cls.__call__, # type: ignore
|
|
112
|
+
to_filter_predicate,
|
|
113
|
+
tree,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
ExceptionFinder.visited = set() # clean up memory
|
|
118
|
+
return result
|
|
92
119
|
|
|
93
|
-
|
|
120
|
+
|
|
121
|
+
def is_absolutely_callable_object(obj: Callable) -> bool:
|
|
122
|
+
return (
|
|
123
|
+
inspect.isclass(type(obj))
|
|
124
|
+
and not inspect.isfunction(obj)
|
|
125
|
+
and not inspect.ismethod(obj)
|
|
126
|
+
)
|
|
94
127
|
|
|
95
128
|
|
|
96
129
|
def _find_explicit_expection_recursively(
|
|
97
130
|
func_obj: Callable,
|
|
98
|
-
expr: ast.AST,
|
|
99
131
|
to_filter_predicate: Callable[[str], bool],
|
|
100
|
-
|
|
132
|
+
tree: Optional[ast.AST] = None,
|
|
133
|
+
) -> list[tuple[Optional[type[Exception]], ast.Raise]]:
|
|
101
134
|
if func_obj in ExceptionFinder.visited:
|
|
102
135
|
return []
|
|
103
136
|
|
|
@@ -108,9 +141,38 @@ def _find_explicit_expection_recursively(
|
|
|
108
141
|
if module and to_filter_predicate(module.__name__) is False:
|
|
109
142
|
return []
|
|
110
143
|
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
if is_absolutely_callable_object(func_obj):
|
|
145
|
+
return _find_all_class_exceptions(type(func_obj), to_filter_predicate, tree)
|
|
146
|
+
|
|
113
147
|
ExceptionFinder.visited.add(func_obj)
|
|
148
|
+
|
|
149
|
+
if tree is None:
|
|
150
|
+
try:
|
|
151
|
+
source = inspect.getsource(func_obj)
|
|
152
|
+
except (OSError, TypeError) as error:
|
|
153
|
+
logger.exception(
|
|
154
|
+
f"Failed to get source code for {get_func_name(func_obj)}.",
|
|
155
|
+
exc_info=error,
|
|
156
|
+
)
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
tree = ast.parse(source)
|
|
161
|
+
except IndentationError as error:
|
|
162
|
+
logger.exception(
|
|
163
|
+
f"Failed to get source code for {get_func_name(func_obj)}.",
|
|
164
|
+
exc_info=error,
|
|
165
|
+
)
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
finder = ExceptionFinder(func_obj, to_filter_predicate)
|
|
169
|
+
try:
|
|
170
|
+
finder.visit(tree)
|
|
171
|
+
except Exception as error:
|
|
172
|
+
logger.exception(
|
|
173
|
+
f"Failed to analyze function {get_func_name(func_obj)}.", exc_info=error
|
|
174
|
+
)
|
|
175
|
+
|
|
114
176
|
return finder.exceptions
|
|
115
177
|
|
|
116
178
|
|
|
@@ -214,7 +276,6 @@ def _resolve_function_from_call_node(
|
|
|
214
276
|
): # don't need to log -> we already know this is class
|
|
215
277
|
return None
|
|
216
278
|
|
|
217
|
-
print(attr_chain, parent_part)
|
|
218
279
|
logger.debug(
|
|
219
280
|
f"Failed to resolve parent attribute {parent_part} for parts {attr_chain}. Chain: {ast.dump(node)} for func {func.__name__}"
|
|
220
281
|
)
|
|
@@ -303,7 +364,7 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
303
364
|
def __init__(
|
|
304
365
|
self, func: Callable, should_search_module_pred: Callable[[str], bool]
|
|
305
366
|
):
|
|
306
|
-
self.exceptions:
|
|
367
|
+
self.exceptions: list[tuple[Optional[type[Exception]], ast.Raise]] = []
|
|
307
368
|
self.assignments: dict[NodeIdentifier, str] = {}
|
|
308
369
|
self.func = func
|
|
309
370
|
self.func_globals = func.__globals__
|
|
@@ -420,9 +481,17 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
420
481
|
return
|
|
421
482
|
|
|
422
483
|
source = inspect.getsource(module)
|
|
423
|
-
|
|
484
|
+
try:
|
|
485
|
+
tree = ast.parse(source)
|
|
486
|
+
except BaseException as error:
|
|
487
|
+
logger.exception(
|
|
488
|
+
f"Failed to parse source code for {func_obj.__name__}.",
|
|
489
|
+
exc_info=error,
|
|
490
|
+
)
|
|
491
|
+
self.generic_visit(node)
|
|
492
|
+
return
|
|
424
493
|
|
|
425
|
-
|
|
494
|
+
founded_func_ast = _find_in_module(
|
|
426
495
|
tree,
|
|
427
496
|
lambda n: (
|
|
428
497
|
isinstance(n, ast.AsyncFunctionDef)
|
|
@@ -430,9 +499,9 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
430
499
|
)
|
|
431
500
|
and n.name == func_obj.__name__,
|
|
432
501
|
)
|
|
433
|
-
if
|
|
502
|
+
if founded_func_ast:
|
|
434
503
|
excs = _find_explicit_expection_recursively(
|
|
435
|
-
func_obj,
|
|
504
|
+
func_obj, self.should_search_module_pred, founded_func_ast
|
|
436
505
|
)
|
|
437
506
|
self.exceptions.extend(excs)
|
|
438
507
|
else:
|
|
@@ -476,7 +545,16 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
476
545
|
return
|
|
477
546
|
|
|
478
547
|
source = inspect.getsource(module)
|
|
479
|
-
|
|
548
|
+
try:
|
|
549
|
+
tree = ast.parse(source)
|
|
550
|
+
except BaseException as error:
|
|
551
|
+
logger.exception(
|
|
552
|
+
f"Failed to parse source code for {caller_type.__name__}.",
|
|
553
|
+
exc_info=error,
|
|
554
|
+
)
|
|
555
|
+
self.generic_visit(node)
|
|
556
|
+
return
|
|
557
|
+
|
|
480
558
|
cls_ast = _find_in_module(
|
|
481
559
|
tree,
|
|
482
560
|
lambda n: isinstance(n, ast.ClassDef)
|
|
@@ -484,10 +562,10 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
484
562
|
)
|
|
485
563
|
if cls_ast:
|
|
486
564
|
casted_cls_ast = typing.cast(ast.ClassDef, cls_ast)
|
|
487
|
-
|
|
488
|
-
if
|
|
565
|
+
submethod_ast = _find_method_node_in_cls(casted_cls_ast, node.attr)
|
|
566
|
+
if submethod_ast:
|
|
489
567
|
excs = _find_explicit_expection_recursively(
|
|
490
|
-
method_func,
|
|
568
|
+
method_func, self.should_search_module_pred, submethod_ast
|
|
491
569
|
)
|
|
492
570
|
self.exceptions.extend(excs)
|
|
493
571
|
|
|
@@ -72,6 +72,9 @@ def compile_openapi_from_fastapi(
|
|
|
72
72
|
routes=app.routes,
|
|
73
73
|
),
|
|
74
74
|
) -> dict:
|
|
75
|
+
target_module = [target_module] if isinstance(target_module, str) else target_module
|
|
76
|
+
target_module.append("fastapi")
|
|
77
|
+
|
|
75
78
|
openapi_schema = open_api_getter(app)
|
|
76
79
|
for route in app.routes:
|
|
77
80
|
if not isinstance(route, APIRoute):
|
|
@@ -209,6 +212,8 @@ def _fill_openapi_with_excpetions(
|
|
|
209
212
|
route: APIRoute,
|
|
210
213
|
exceptions: list[tuple[type[StarletteHTTPException], ast.Raise]],
|
|
211
214
|
) -> None:
|
|
215
|
+
added_schema_names = set()
|
|
216
|
+
|
|
212
217
|
for exc, ast_node in exceptions:
|
|
213
218
|
if hasattr(exc, "get_json_schema"):
|
|
214
219
|
casted_exc = typing.cast(type[BaseHTTPException], exc)
|
|
@@ -216,9 +221,11 @@ def _fill_openapi_with_excpetions(
|
|
|
216
221
|
else:
|
|
217
222
|
schema = _extract_json_schema(ast_node, exc)
|
|
218
223
|
|
|
219
|
-
if schema is None:
|
|
224
|
+
if schema is None or schema.schema_name in added_schema_names:
|
|
220
225
|
continue
|
|
221
226
|
|
|
227
|
+
added_schema_names.add(schema.schema_name)
|
|
228
|
+
|
|
222
229
|
str_status_code = str(schema.status_code)
|
|
223
230
|
component_schemas = api_schema.get("components", {}).get("schemas", {})
|
|
224
231
|
if schema.schema_name not in component_schemas:
|
|
@@ -290,7 +297,7 @@ def flatten(to_be_flatten: list[list[T]]) -> list[T]:
|
|
|
290
297
|
|
|
291
298
|
|
|
292
299
|
def _extract_starlette_exceptions(
|
|
293
|
-
route: APIRoute, target_module:
|
|
300
|
+
route: APIRoute, target_module: list[str]
|
|
294
301
|
) -> list[tuple[type[StarletteHTTPException], ast.Raise]]:
|
|
295
302
|
dependency_tree = build_dependency_tree(route.dependant)
|
|
296
303
|
exceptions = flatten(
|
|
@@ -33,6 +33,17 @@ class BaseHTTPException(HTTPException):
|
|
|
33
33
|
return value
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def try_to_camel_case(string: str) -> str:
|
|
37
|
+
if " " in string: # with space
|
|
38
|
+
components = string.split(" ")
|
|
39
|
+
return components[0] + "".join(x.title() for x in components[1:])
|
|
40
|
+
elif "_" in string: # snake case
|
|
41
|
+
components = string.split("_")
|
|
42
|
+
return components[0] + "".join(x.title() for x in components[1:])
|
|
43
|
+
else:
|
|
44
|
+
return string # not known
|
|
45
|
+
|
|
46
|
+
|
|
36
47
|
def _generic_json_schema_builder(
|
|
37
48
|
cls: type[Exception],
|
|
38
49
|
detail: str | None = None,
|
|
@@ -51,6 +62,8 @@ def _generic_json_schema_builder(
|
|
|
51
62
|
|
|
52
63
|
if detail is not None:
|
|
53
64
|
detail_type = Literal[detail] # type: ignore
|
|
65
|
+
|
|
66
|
+
name = f"{try_to_camel_case(detail)}Schema"
|
|
54
67
|
else:
|
|
55
68
|
detail_type = str
|
|
56
69
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import sys
|
|
3
2
|
from functools import lru_cache
|
|
4
3
|
|
|
5
4
|
from fastapi import Depends, FastAPI, HTTPException, status
|
|
@@ -10,12 +9,8 @@ from richapi.exc_parser.protocol import BaseHTTPException
|
|
|
10
9
|
|
|
11
10
|
app = FastAPI()
|
|
12
11
|
|
|
13
|
-
root = logging.getLogger()
|
|
14
|
-
root.setLevel(logging.DEBUG)
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
handler.setLevel(logging.DEBUG)
|
|
18
|
-
root.addHandler(handler)
|
|
13
|
+
logging.getLogger().addHandler(logging.StreamHandler())
|
|
19
14
|
|
|
20
15
|
|
|
21
16
|
class FirstException(HTTPException): ...
|
|
@@ -104,12 +99,3 @@ def test_case():
|
|
|
104
99
|
assert "407" in home_responses
|
|
105
100
|
assert "408" in home_responses
|
|
106
101
|
assert "409" in home_responses
|
|
107
|
-
schemas = openapi_json["components"]["schemas"]
|
|
108
|
-
assert "SecondExceptionErrorSchema" in schemas
|
|
109
|
-
assert "ThirdExceptionErrorSchema" in schemas
|
|
110
|
-
assert "FourthExceptionErrorSchema" in schemas
|
|
111
|
-
assert "FifthExceptionErrorSchema" in schemas
|
|
112
|
-
assert "SixthExceptionErrorSchema" in schemas
|
|
113
|
-
assert "NoNeedParanthesisErrorSchema" in schemas
|
|
114
|
-
assert "SeventhExceptionErrorSchema" in schemas
|
|
115
|
-
assert "EighthExceptionErrorSchema" in schemas
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fastapi
|
|
2
|
+
import fastapi.security
|
|
3
|
+
|
|
4
|
+
from richapi.exc_parser.openapi import compile_openapi_from_fastapi
|
|
5
|
+
|
|
6
|
+
app = fastapi.FastAPI()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
http_bearer = fastapi.security.HTTPBearer()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_at_11(
|
|
13
|
+
token: fastapi.security.HTTPAuthorizationCredentials = fastapi.Depends(http_bearer),
|
|
14
|
+
) -> str:
|
|
15
|
+
return "12312"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.get("/refresh")
|
|
19
|
+
async def refresh_token(
|
|
20
|
+
token_data: str = fastapi.Depends(get_at_11),
|
|
21
|
+
) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_fastapi_call_detected():
|
|
26
|
+
openapi_json = compile_openapi_from_fastapi(
|
|
27
|
+
app, target_module="tests.test_fastapi_auth_functions"
|
|
28
|
+
)
|
|
29
|
+
home_responses = openapi_json["paths"]["/refresh"]["get"]["responses"]
|
|
30
|
+
assert "403" in home_responses
|
|
@@ -327,19 +327,6 @@ wheels = [
|
|
|
327
327
|
{ url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 },
|
|
328
328
|
]
|
|
329
329
|
|
|
330
|
-
[[package]]
|
|
331
|
-
name = "loguru"
|
|
332
|
-
version = "0.7.2"
|
|
333
|
-
source = { registry = "https://pypi.org/simple" }
|
|
334
|
-
dependencies = [
|
|
335
|
-
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
336
|
-
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
|
337
|
-
]
|
|
338
|
-
sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103 }
|
|
339
|
-
wheels = [
|
|
340
|
-
{ url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549 },
|
|
341
|
-
]
|
|
342
|
-
|
|
343
330
|
[[package]]
|
|
344
331
|
name = "markdown"
|
|
345
332
|
version = "3.7"
|
|
@@ -1071,13 +1058,12 @@ name = "richapi"
|
|
|
1071
1058
|
version = "0.1.0"
|
|
1072
1059
|
source = { editable = "." }
|
|
1073
1060
|
dependencies = [
|
|
1074
|
-
{ name = "
|
|
1061
|
+
{ name = "fastapi" },
|
|
1075
1062
|
{ name = "typer" },
|
|
1076
1063
|
]
|
|
1077
1064
|
|
|
1078
1065
|
[package.dev-dependencies]
|
|
1079
1066
|
dev = [
|
|
1080
|
-
{ name = "fastapi" },
|
|
1081
1067
|
{ name = "httpx" },
|
|
1082
1068
|
{ name = "mdx-truly-sane-lists" },
|
|
1083
1069
|
{ name = "mike" },
|
|
@@ -1098,13 +1084,12 @@ dev = [
|
|
|
1098
1084
|
|
|
1099
1085
|
[package.metadata]
|
|
1100
1086
|
requires-dist = [
|
|
1101
|
-
{ name = "
|
|
1102
|
-
{ name = "typer", specifier = ">=0.12
|
|
1087
|
+
{ name = "fastapi", specifier = ">=0.105" },
|
|
1088
|
+
{ name = "typer", specifier = ">=0.12" },
|
|
1103
1089
|
]
|
|
1104
1090
|
|
|
1105
1091
|
[package.metadata.requires-dev]
|
|
1106
1092
|
dev = [
|
|
1107
|
-
{ name = "fastapi", specifier = ">=0.115.0" },
|
|
1108
1093
|
{ name = "httpx", specifier = ">=0.27.2" },
|
|
1109
1094
|
{ name = "mdx-truly-sane-lists", specifier = ">=1.3" },
|
|
1110
1095
|
{ name = "mike", specifier = "==2.0.0" },
|
|
@@ -1326,15 +1311,6 @@ wheels = [
|
|
|
1326
1311
|
{ url = "https://files.pythonhosted.org/packages/38/b8/0aa69337651b3005f161f7f494e59188a1d8d94171666900d26d29d10f69/watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45", size = 79324 },
|
|
1327
1312
|
]
|
|
1328
1313
|
|
|
1329
|
-
[[package]]
|
|
1330
|
-
name = "win32-setctime"
|
|
1331
|
-
version = "1.1.0"
|
|
1332
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1333
|
-
sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 }
|
|
1334
|
-
wheels = [
|
|
1335
|
-
{ url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 },
|
|
1336
|
-
]
|
|
1337
|
-
|
|
1338
1314
|
[[package]]
|
|
1339
1315
|
name = "zipp"
|
|
1340
1316
|
version = "3.20.2"
|
richapi-0.1.0/.python-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.9.19
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|