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.
Files changed (33) hide show
  1. {richapi-0.1.0 → richapi-0.2.0}/.gitignore +1 -0
  2. {richapi-0.1.0 → richapi-0.2.0}/PKG-INFO +4 -3
  3. {richapi-0.1.0 → richapi-0.2.0}/README.md +1 -0
  4. {richapi-0.1.0 → richapi-0.2.0}/docs/advance.md +18 -0
  5. {richapi-0.1.0 → richapi-0.2.0}/docs/conventions.md +24 -5
  6. richapi-0.2.0/docs/implementation.md +55 -0
  7. {richapi-0.1.0 → richapi-0.2.0}/docs/index.md +2 -2
  8. {richapi-0.1.0 → richapi-0.2.0}/mkdocs.yml +1 -0
  9. {richapi-0.1.0 → richapi-0.2.0}/pyproject.toml +2 -3
  10. richapi-0.2.0/richapi/__version__.py +1 -0
  11. {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/compiler.py +101 -23
  12. {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/openapi.py +9 -2
  13. {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/protocol.py +13 -0
  14. {richapi-0.1.0 → richapi-0.2.0}/tests/test_exc_raising_cases.py +1 -15
  15. richapi-0.2.0/tests/test_fastapi_auth_functions.py +30 -0
  16. {richapi-0.1.0 → richapi-0.2.0}/uv.lock +3 -27
  17. richapi-0.1.0/.python-version +0 -1
  18. richapi-0.1.0/richapi/__version__.py +0 -1
  19. {richapi-0.1.0 → richapi-0.2.0}/.envrc +0 -0
  20. {richapi-0.1.0 → richapi-0.2.0}/.github/workflows/deploy-docs-on-demand.yml +0 -0
  21. {richapi-0.1.0 → richapi-0.2.0}/.github/workflows/main.yml +0 -0
  22. {richapi-0.1.0 → richapi-0.2.0}/.github/workflows/on-release-main.yml +0 -0
  23. {richapi-0.1.0 → richapi-0.2.0}/deploy_docs.py +0 -0
  24. {richapi-0.1.0 → richapi-0.2.0}/docs/images/with-richapi.png +0 -0
  25. {richapi-0.1.0 → richapi-0.2.0}/docs/images/without-richapi.png +0 -0
  26. {richapi-0.1.0 → richapi-0.2.0}/makefile +0 -0
  27. {richapi-0.1.0 → richapi-0.2.0}/richapi/__init__.py +0 -0
  28. {richapi-0.1.0 → richapi-0.2.0}/richapi/cli.py +0 -0
  29. {richapi-0.1.0 → richapi-0.2.0}/richapi/exc_parser/__init__.py +0 -0
  30. {richapi-0.1.0 → richapi-0.2.0}/richapi/exceptions.py +0 -0
  31. {richapi-0.1.0 → richapi-0.2.0}/tests/__init__.py +0 -0
  32. {richapi-0.1.0 → richapi-0.2.0}/tests/test_exception_edge_cases.py +0 -0
  33. {richapi-0.1.0 → richapi-0.2.0}/tox.ini +0 -0
@@ -6,6 +6,7 @@ docs/source
6
6
  __pycache__/
7
7
  *.py[cod]
8
8
  *$py.class
9
+ .python_version
9
10
 
10
11
  # C extensions
11
12
  *.so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: richapi
3
- Version: 0.1.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: loguru>=0.7.2
12
- Requires-Dist: typer>=0.12.5
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
- ## Conventions
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
- ### Inherit from `richapi.BaseHTTPException`
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
- ### Avoiding dynamic data in the constructor of the exceptions
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
- ### Avoiding usage of magic methods
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
- ### Keep it simple
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
- **RichAPI is designed to simplify FastAPI projects by automatically generating documented HTTP responses from HTTPException without extra effort! 🎉
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,
@@ -103,3 +103,4 @@ nav:
103
103
  - Welcome: index.md
104
104
  - Conventions: conventions.md
105
105
  - Advance Usage: advance.md
106
+ - Implemenetion Detail: implementation.md
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "richapi"
3
- version = "0.1.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 = ["loguru>=0.7.2", "typer>=0.12.5"]
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: Union[list[str], str],
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
- source = inspect.getsource(func)
88
- except (OSError, TypeError):
89
- raise Exception(func)
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
- tree = ast.parse(source)
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
- return _find_explicit_expection_recursively(func, tree, should_search_module_pred)
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
- finder = ExceptionFinder(func_obj, to_filter_predicate)
112
- finder.visit(expr)
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: List[Tuple[Optional[type[Exception]], ast.Raise]] = []
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
- tree = ast.parse(source)
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
- founded_func = _find_in_module(
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 founded_func:
502
+ if founded_func_ast:
434
503
  excs = _find_explicit_expection_recursively(
435
- func_obj, founded_func, self.should_search_module_pred
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
- tree = ast.parse(source)
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
- submethod = _find_method_node_in_cls(casted_cls_ast, node.attr)
488
- if submethod:
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, submethod, self.should_search_module_pred
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: Union[list[str], str]
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
- handler = logging.StreamHandler(sys.stdout)
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 = "loguru" },
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 = "loguru", specifier = ">=0.7.2" },
1102
- { name = "typer", specifier = ">=0.12.5" },
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"
@@ -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