richapi 0.2.1__tar.gz → 0.3.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.2.1 → richapi-0.3.0}/PKG-INFO +1 -1
- {richapi-0.2.1 → richapi-0.3.0}/docs/advance.md +1 -4
- {richapi-0.2.1 → richapi-0.3.0}/docs/conventions.md +1 -1
- richapi-0.3.0/docs/images/custom-exception.png +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/docs/index.md +51 -4
- {richapi-0.2.1 → richapi-0.3.0}/pyproject.toml +1 -1
- richapi-0.3.0/richapi/__init__.py +11 -0
- richapi-0.3.0/richapi/__version__.py +1 -0
- {richapi-0.2.1 → richapi-0.3.0}/richapi/exc_parser/compiler.py +36 -13
- richapi-0.3.0/richapi/exc_parser/handler.py +22 -0
- {richapi-0.2.1 → richapi-0.3.0}/richapi/exc_parser/openapi.py +5 -0
- {richapi-0.2.1 → richapi-0.3.0}/richapi/exc_parser/protocol.py +32 -3
- richapi-0.3.0/tests/test_custom_exc.py +52 -0
- richapi-0.2.1/richapi/__init__.py +0 -4
- richapi-0.2.1/richapi/__version__.py +0 -1
- {richapi-0.2.1 → richapi-0.3.0}/.envrc +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/.github/workflows/deploy-docs-on-demand.yml +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/.github/workflows/main.yml +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/.github/workflows/on-release-main.yml +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/.gitignore +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/README.md +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/deploy_docs.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/docs/images/with-richapi.png +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/docs/images/without-richapi.png +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/docs/implementation.md +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/makefile +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/mkdocs.yml +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/richapi/cli.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/richapi/exc_parser/__init__.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/richapi/exceptions.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/tests/__init__.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/tests/test_exc_raising_cases.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/tests/test_exception_edge_cases.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/tests/test_fastapi_auth_functions.py +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/tox.ini +0 -0
- {richapi-0.2.1 → richapi-0.3.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: richapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -4,9 +4,6 @@ This is guideline for how to use the library in more advanced ways.
|
|
|
4
4
|
|
|
5
5
|
### Customization of the OpenAPI schema
|
|
6
6
|
|
|
7
|
-
Sometimes, your exceptions is more than 'details'.
|
|
8
|
-
In that case, you can very explicitly define the exception JSON schema inside the exception class. You may even use Pydantic to define the schema dynamically so you don't need to write the schema manually.
|
|
9
|
-
|
|
10
7
|
All you need to do, is to return type HTTPExceptionSchema correctly on the method `get_json_schema` of your exception class which you inherited from `BaseHTTPException`.
|
|
11
8
|
|
|
12
9
|
Imagine you are writing an exception that is returning a user balance and a time to retry again a failed bank transaction, you can define the schema like this:
|
|
@@ -33,7 +30,7 @@ class YourException(BaseHTTPException):
|
|
|
33
30
|
|
|
34
31
|
```
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
If your intention is to have a custom response schema, then recommended way is to use [custom exception feature](index.md#customization-of-exception).
|
|
37
34
|
|
|
38
35
|
### Finding exceptions in other modules
|
|
39
36
|
|
|
@@ -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` or `richapi.RichHTTPException`
|
|
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.
|
|
Binary file
|
|
@@ -46,6 +46,57 @@ with open("openapi.json") as open_api_json:
|
|
|
46
46
|
|
|
47
47
|
I personally recommend the second way, because it is better to have it compiled and saved to file, because there are static analysis running on the OpanAPI generator and your source code which isn't very ideal to be running in production.
|
|
48
48
|
|
|
49
|
+
### Customization of Exception
|
|
50
|
+
|
|
51
|
+
Sometimes, your exceptions is more than 'details'. You may want to put some extra information for the client to understand and parse the error better. In that case, please follow these steps:
|
|
52
|
+
|
|
53
|
+
1. Add the exception handler from richapi to your FastAPI app.
|
|
54
|
+
2. Inhert from `richapi.RichHTTPException`, and make it a dataclass.
|
|
55
|
+
3. To override status, simply set the `status_code` attribute on the class without type annotation.
|
|
56
|
+
4. Raise that exception somewhere in your code and you'll see the schema on swagger!
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import fastapi
|
|
62
|
+
import fastapi.security
|
|
63
|
+
from pydantic import BaseModel
|
|
64
|
+
from pydantic.dataclasses import dataclass
|
|
65
|
+
|
|
66
|
+
from richapi.exc_parser.handler import add_exc_handler
|
|
67
|
+
from richapi.exc_parser.openapi import enrich_openapi
|
|
68
|
+
from richapi.exc_parser.protocol import RichHTTPException
|
|
69
|
+
|
|
70
|
+
app = fastapi.FastAPI()
|
|
71
|
+
app.openapi = enrich_openapi(app)
|
|
72
|
+
add_exc_handler(app)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class NotEnoughBalance(RichHTTPException):
|
|
77
|
+
"""Custom exception for user balance"""
|
|
78
|
+
|
|
79
|
+
user_id: int
|
|
80
|
+
balance: float
|
|
81
|
+
status_code = 403
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SuccessResponse(BaseModel):
|
|
85
|
+
balance: float
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.post("/payment")
|
|
89
|
+
async def make_payment() -> SuccessResponse:
|
|
90
|
+
raise NotEnoughBalance(user_id=1, balance=0.5)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Final Result:
|
|
94
|
+

|
|
95
|
+
|
|
96
|
+
### Conventions (VERY IMPORTANT)
|
|
97
|
+
|
|
98
|
+
There are some conventions about how to raise exceptions and etc. You may read the [conventions](conventions.md) to know more about them. The way library works is that it is a mini compiler using Abstract Syntax Tree and functions global to extract exceptions and their responses, and then it generates the OpenAPI schema from them. So there are limitations to what it can do, and you may need to follow the conventions to get the best out of it. Anything that is not following the conventions MAY be ignored by the compiler, and there is no plan to support them in the future, because it is not possible to support all the ways you can raise exceptions in Python... To give you an example, you may download a source code from internet and then use 'eval' function to run it dynamically, and there is no way to know what exceptions you may raise in that code, so it is impossible to support all the ways you can raise exceptions in Python.
|
|
99
|
+
|
|
49
100
|
### Example
|
|
50
101
|
|
|
51
102
|
Imagine this application;
|
|
@@ -105,7 +156,3 @@ What would you see WITHOUT RichAPI:
|
|
|
105
156
|
|
|
106
157
|
After compiling OpenAPI WITH RichAPI (extended with all possible exceptions coming from your router):
|
|
107
158
|

|
|
108
|
-
|
|
109
|
-
### Conventions (VERY IMPORTANT)
|
|
110
|
-
|
|
111
|
-
There are some conventions about how to raise exceptions and etc. You may read the [conventions](conventions.md) to know more about them. The way library works is that it is a mini compiler using Abstract Syntax Tree and functions global to extract exceptions and their responses, and then it generates the OpenAPI schema from them. So there are limitations to what it can do, and you may need to follow the conventions to get the best out of it. Anything that is not following the conventions MAY be ignored by the compiler, and there is no plan to support them in the future, because it is not possible to support all the ways you can raise exceptions in Python... To give you an example, you may download a source code from internet and then use 'eval' function to run it dynamically, and there is no way to know what exceptions you may raise in that code, so it is impossible to support all the ways you can raise exceptions in Python.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from richapi.exc_parser.handler import add_exc_handler
|
|
2
|
+
from richapi.exc_parser.openapi import enrich_openapi, load_openapi
|
|
3
|
+
from richapi.exc_parser.protocol import BaseHTTPException, RichHTTPException
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"enrich_openapi",
|
|
7
|
+
"load_openapi",
|
|
8
|
+
"BaseHTTPException",
|
|
9
|
+
"RichHTTPException",
|
|
10
|
+
"add_exc_handler",
|
|
11
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -95,10 +95,14 @@ def _find_all_class_exceptions(
|
|
|
95
95
|
try:
|
|
96
96
|
tree = tree or ast.parse(inspect.getsource(cls))
|
|
97
97
|
except Exception as error:
|
|
98
|
-
logger.exception(
|
|
98
|
+
logger.exception(
|
|
99
|
+
f"Failed to parse source code for {cls.__name__}.", exc_info=error
|
|
100
|
+
)
|
|
99
101
|
return []
|
|
100
102
|
|
|
101
|
-
result = _find_explicit_expection_recursively(
|
|
103
|
+
result = _find_explicit_expection_recursively(
|
|
104
|
+
cls.__init__, to_filter_predicate, tree
|
|
105
|
+
)
|
|
102
106
|
if hasattr(cls, "__call__"):
|
|
103
107
|
# Not sure if __call__ was called or init, so check for both
|
|
104
108
|
|
|
@@ -110,13 +114,14 @@ def _find_all_class_exceptions(
|
|
|
110
114
|
)
|
|
111
115
|
)
|
|
112
116
|
|
|
113
|
-
ExceptionFinder.visited = set() # clean up memory
|
|
114
117
|
return result
|
|
115
118
|
|
|
116
119
|
|
|
117
120
|
def is_absolutely_callable_object(obj: Callable) -> bool:
|
|
118
121
|
return (
|
|
119
|
-
inspect.isclass(type(obj))
|
|
122
|
+
inspect.isclass(type(obj))
|
|
123
|
+
and (not inspect.isfunction(obj))
|
|
124
|
+
and (not inspect.ismethod(obj))
|
|
120
125
|
)
|
|
121
126
|
|
|
122
127
|
|
|
@@ -169,7 +174,9 @@ def _find_explicit_expection_recursively(
|
|
|
169
174
|
try:
|
|
170
175
|
finder.visit(tree)
|
|
171
176
|
except Exception as error:
|
|
172
|
-
logger.exception(
|
|
177
|
+
logger.exception(
|
|
178
|
+
f"Failed to analyze function {get_func_name(func_obj)}.", exc_info=error
|
|
179
|
+
)
|
|
173
180
|
|
|
174
181
|
return finder.exceptions
|
|
175
182
|
|
|
@@ -247,7 +254,9 @@ def _resolve_function_from_call_node(
|
|
|
247
254
|
elif isinstance(node, ast.Attribute):
|
|
248
255
|
attr_chain = _resolve_full_attribute_path(node)
|
|
249
256
|
if attr_chain is None:
|
|
250
|
-
logger.debug(
|
|
257
|
+
logger.debug(
|
|
258
|
+
f"Failed to get full attribute name: {ast.dump(node)} in {func.__name__}"
|
|
259
|
+
)
|
|
251
260
|
return None
|
|
252
261
|
|
|
253
262
|
try:
|
|
@@ -267,7 +276,9 @@ def _resolve_function_from_call_node(
|
|
|
267
276
|
parent_attr = assignments.get(node_id, None)
|
|
268
277
|
|
|
269
278
|
if parent_attr is None:
|
|
270
|
-
if
|
|
279
|
+
if (
|
|
280
|
+
parent_part == "self"
|
|
281
|
+
): # don't need to log -> we already know this is class
|
|
271
282
|
return None
|
|
272
283
|
|
|
273
284
|
logger.debug(
|
|
@@ -295,7 +306,9 @@ def _extract_node_name(
|
|
|
295
306
|
Extract the exception name a node recursively.
|
|
296
307
|
"""
|
|
297
308
|
if isinstance(node, ast.Call):
|
|
298
|
-
return _extract_node_name(
|
|
309
|
+
return _extract_node_name(
|
|
310
|
+
typing.cast(SupportedAstNodes, node.func), assignments
|
|
311
|
+
)
|
|
299
312
|
elif isinstance(node, ast.Name):
|
|
300
313
|
var_name = get_node_identifier(node)
|
|
301
314
|
return assignments.get(var_name, var_name)
|
|
@@ -303,7 +316,9 @@ def _extract_node_name(
|
|
|
303
316
|
return _resolve_full_attribute_path(node)
|
|
304
317
|
|
|
305
318
|
elif isinstance(node, ast.Await):
|
|
306
|
-
return _extract_node_name(
|
|
319
|
+
return _extract_node_name(
|
|
320
|
+
typing.cast(SupportedAstNodes, node.value), assignments
|
|
321
|
+
)
|
|
307
322
|
|
|
308
323
|
elif isinstance(
|
|
309
324
|
node,
|
|
@@ -351,7 +366,9 @@ def is_stdlib(module_name: str) -> bool:
|
|
|
351
366
|
class ExceptionFinder(ast.NodeVisitor):
|
|
352
367
|
visited: set[Callable] = set()
|
|
353
368
|
|
|
354
|
-
def __init__(
|
|
369
|
+
def __init__(
|
|
370
|
+
self, func: Callable, should_search_module_pred: Callable[[str], bool]
|
|
371
|
+
):
|
|
355
372
|
self.exceptions: list[tuple[Optional[type[Exception]], ast.Raise]] = []
|
|
356
373
|
self.assignments: dict[NodeIdentifier, str] = {}
|
|
357
374
|
self.func = func
|
|
@@ -481,7 +498,10 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
481
498
|
|
|
482
499
|
founded_func_ast = _find_in_module(
|
|
483
500
|
tree,
|
|
484
|
-
lambda n: (
|
|
501
|
+
lambda n: (
|
|
502
|
+
isinstance(n, ast.AsyncFunctionDef)
|
|
503
|
+
or isinstance(n, ast.FunctionDef)
|
|
504
|
+
)
|
|
485
505
|
and n.name == func_obj.__name__,
|
|
486
506
|
)
|
|
487
507
|
if founded_func_ast:
|
|
@@ -509,7 +529,9 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
509
529
|
try:
|
|
510
530
|
caller_type = _exctact_type(caller_node_name, self.func_globals)
|
|
511
531
|
except Exception:
|
|
512
|
-
logger.critical(
|
|
532
|
+
logger.critical(
|
|
533
|
+
f"Failed to resolve attribute chain: {caller_node_name}"
|
|
534
|
+
)
|
|
513
535
|
self.generic_visit(node)
|
|
514
536
|
return
|
|
515
537
|
|
|
@@ -540,7 +562,8 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
540
562
|
|
|
541
563
|
cls_ast = _find_in_module(
|
|
542
564
|
tree,
|
|
543
|
-
lambda n: isinstance(n, ast.ClassDef)
|
|
565
|
+
lambda n: isinstance(n, ast.ClassDef)
|
|
566
|
+
and n.name == caller_type.__name__,
|
|
544
567
|
)
|
|
545
568
|
if cls_ast:
|
|
546
569
|
casted_cls_ast = typing.cast(ast.ClassDef, cls_ast)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
from pydantic import TypeAdapter
|
|
4
|
+
from starlette.requests import Request
|
|
5
|
+
from starlette.responses import Response
|
|
6
|
+
|
|
7
|
+
from richapi.exc_parser.protocol import RichHTTPException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def _handle_rich_http_exception(request: Request, exc: Exception) -> Response:
|
|
11
|
+
assert isinstance(exc, RichHTTPException)
|
|
12
|
+
|
|
13
|
+
adapter = TypeAdapter(exc.__class__)
|
|
14
|
+
if exc._validate_arg_types:
|
|
15
|
+
adapter.validate_python(exc)
|
|
16
|
+
|
|
17
|
+
json_response = adapter.dump_python(exc)
|
|
18
|
+
return JSONResponse(content=json_response, status_code=exc.status_code)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_exc_handler(app: FastAPI):
|
|
22
|
+
app.add_exception_handler(RichHTTPException, handler=_handle_rich_http_exception)
|
|
@@ -12,6 +12,9 @@ from fastapi.openapi.utils import get_openapi as _get_openapi
|
|
|
12
12
|
from fastapi.routing import APIRoute
|
|
13
13
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
14
14
|
|
|
15
|
+
from richapi.exc_parser.compiler import (
|
|
16
|
+
ExceptionFinder,
|
|
17
|
+
)
|
|
15
18
|
from richapi.exc_parser.compiler import (
|
|
16
19
|
find_explicit_exceptions as _find_explicit_exceptions,
|
|
17
20
|
)
|
|
@@ -88,6 +91,8 @@ def compile_openapi_from_fastapi(
|
|
|
88
91
|
|
|
89
92
|
_fill_openapi_with_excpetions(openapi_schema, route, exceptions)
|
|
90
93
|
|
|
94
|
+
ExceptionFinder.visited.clear()
|
|
95
|
+
|
|
91
96
|
return openapi_schema
|
|
92
97
|
|
|
93
98
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, Literal
|
|
3
|
+
from typing import Any, ClassVar, Literal
|
|
4
4
|
|
|
5
5
|
from fastapi import HTTPException
|
|
6
|
-
from pydantic import BaseModel, create_model
|
|
6
|
+
from pydantic import BaseModel, TypeAdapter, create_model
|
|
7
|
+
from pydantic.dataclasses import dataclass
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class HTTPExceptionSchema(BaseModel):
|
|
@@ -27,10 +28,38 @@ class BaseHTTPException(HTTPException):
|
|
|
27
28
|
@classmethod
|
|
28
29
|
def get_json_schema(cls) -> HTTPExceptionSchema:
|
|
29
30
|
value = _generic_json_schema_builder(cls)
|
|
30
|
-
assert
|
|
31
|
+
assert (
|
|
32
|
+
value is not None
|
|
33
|
+
) # should never happen because status code is always set
|
|
31
34
|
return value
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
@dataclass
|
|
38
|
+
class RichHTTPException(HTTPException):
|
|
39
|
+
status_code: ClassVar[int] = 422
|
|
40
|
+
"""HTTP Status code to raise"""
|
|
41
|
+
|
|
42
|
+
_validate_arg_types: ClassVar[bool] = True
|
|
43
|
+
"""Whether to run validation on arguements"""
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def get_json_schema(cls) -> HTTPExceptionSchema:
|
|
47
|
+
adapter = TypeAdapter(cls)
|
|
48
|
+
schema = adapter.json_schema()
|
|
49
|
+
schema.pop("status_code", None)
|
|
50
|
+
|
|
51
|
+
schema_desc = cls.__doc__
|
|
52
|
+
if schema_desc == cls.__name__ + "()" or schema_desc is None:
|
|
53
|
+
schema_desc = "No description provided"
|
|
54
|
+
|
|
55
|
+
return HTTPExceptionSchema(
|
|
56
|
+
response_schema_json=schema,
|
|
57
|
+
schema_name=f"{cls.__name__}Schema",
|
|
58
|
+
status_code=cls.status_code,
|
|
59
|
+
schema_desc=schema_desc,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
34
63
|
def try_to_camel_case(string: str) -> str:
|
|
35
64
|
final_str = string
|
|
36
65
|
if " " in final_str: # with space
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fastapi
|
|
2
|
+
import fastapi.security
|
|
3
|
+
import pytest
|
|
4
|
+
from httpx import ASGITransport, AsyncClient
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pydantic.dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from richapi.exc_parser.handler import add_exc_handler
|
|
9
|
+
from richapi.exc_parser.openapi import compile_openapi_from_fastapi, enrich_openapi
|
|
10
|
+
from richapi.exc_parser.protocol import RichHTTPException
|
|
11
|
+
|
|
12
|
+
app = fastapi.FastAPI()
|
|
13
|
+
app.openapi = enrich_openapi(app)
|
|
14
|
+
add_exc_handler(app)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class NotEnoughBalance(RichHTTPException):
|
|
19
|
+
"""Custom exception for user balance"""
|
|
20
|
+
|
|
21
|
+
user_id: int
|
|
22
|
+
balance: float
|
|
23
|
+
status_code = 409
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SuccessResponse(BaseModel):
|
|
27
|
+
balance: float
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.post("/payment")
|
|
31
|
+
async def make_payment() -> SuccessResponse:
|
|
32
|
+
raise NotEnoughBalance(user_id=1, balance=0.5)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_custom_exc_detected():
|
|
36
|
+
openapi_json = compile_openapi_from_fastapi(
|
|
37
|
+
app, target_module="tests.test_custom_exc"
|
|
38
|
+
)
|
|
39
|
+
home_responses = openapi_json["paths"]["/payment"]["post"]["responses"]
|
|
40
|
+
assert "409" in home_responses
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_make_payment():
|
|
45
|
+
async with AsyncClient(
|
|
46
|
+
transport=ASGITransport(app=app), base_url="http://test"
|
|
47
|
+
) as ac:
|
|
48
|
+
response = await ac.post("/payment")
|
|
49
|
+
|
|
50
|
+
assert response.status_code == 409
|
|
51
|
+
expected_response = {"user_id": 1, "balance": 0.5}
|
|
52
|
+
assert response.json() == expected_response
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.1"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|