Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/_compat.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/cli.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/concurrency.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/encoders.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/exception_handlers.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/exceptions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/params.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/responses.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/routing.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/staticfiles.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/types.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/dependencies/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/fastapi/dependencies/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/dependencies/__pycache__/models.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/dependencies/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/dependencies/models.py +37 -0
- .venv/lib/python3.11/site-packages/fastapi/dependencies/utils.py +972 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/constants.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/docs.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/models.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/constants.py +3 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/docs.py +344 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/models.py +445 -0
- .venv/lib/python3.11/site-packages/fastapi/openapi/utils.py +548 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__init__.py +15 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__pycache__/api_key.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__pycache__/base.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__pycache__/http.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__pycache__/oauth2.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__pycache__/open_id_connect_url.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/security/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/fastapi/security/api_key.py +288 -0
- .venv/lib/python3.11/site-packages/fastapi/security/base.py +6 -0
- .venv/lib/python3.11/site-packages/fastapi/security/http.py +420 -0
- .venv/lib/python3.11/site-packages/fastapi/security/oauth2.py +638 -0
- .venv/lib/python3.11/site-packages/fastapi/security/open_id_connect_url.py +84 -0
- .venv/lib/python3.11/site-packages/fastapi/security/utils.py +10 -0
- .venv/lib/python3.11/site-packages/partial_json_parser/__init__.py +7 -0
- .venv/lib/python3.11/site-packages/partial_json_parser/__pycache__/version.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/api.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/complete.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/exceptions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/myelin.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/options.cpython-311.pyc +0 -0
.venv/lib/python3.11/site-packages/fastapi/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (1.39 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/_compat.cpython-311.pyc
ADDED
|
Binary file (32.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/cli.cpython-311.pyc
ADDED
|
Binary file (758 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/concurrency.cpython-311.pyc
ADDED
|
Binary file (1.86 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/encoders.cpython-311.pyc
ADDED
|
Binary file (12.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/exception_handlers.cpython-311.pyc
ADDED
|
Binary file (2.34 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/exceptions.cpython-311.pyc
ADDED
|
Binary file (8.25 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/params.cpython-311.pyc
ADDED
|
Binary file (29.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/responses.cpython-311.pyc
ADDED
|
Binary file (2.75 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/routing.cpython-311.pyc
ADDED
|
Binary file (92.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/staticfiles.cpython-311.pyc
ADDED
|
Binary file (254 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/types.cpython-311.pyc
ADDED
|
Binary file (911 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (9.39 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/dependencies/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/fastapi/dependencies/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (193 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/dependencies/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (3.38 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/dependencies/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (41.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/dependencies/models.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field
|
| 2 |
+
from typing import Any, Callable, List, Optional, Sequence, Tuple
|
| 3 |
+
|
| 4 |
+
from fastapi._compat import ModelField
|
| 5 |
+
from fastapi.security.base import SecurityBase
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class SecurityRequirement:
|
| 10 |
+
security_scheme: SecurityBase
|
| 11 |
+
scopes: Optional[Sequence[str]] = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class Dependant:
|
| 16 |
+
path_params: List[ModelField] = field(default_factory=list)
|
| 17 |
+
query_params: List[ModelField] = field(default_factory=list)
|
| 18 |
+
header_params: List[ModelField] = field(default_factory=list)
|
| 19 |
+
cookie_params: List[ModelField] = field(default_factory=list)
|
| 20 |
+
body_params: List[ModelField] = field(default_factory=list)
|
| 21 |
+
dependencies: List["Dependant"] = field(default_factory=list)
|
| 22 |
+
security_requirements: List[SecurityRequirement] = field(default_factory=list)
|
| 23 |
+
name: Optional[str] = None
|
| 24 |
+
call: Optional[Callable[..., Any]] = None
|
| 25 |
+
request_param_name: Optional[str] = None
|
| 26 |
+
websocket_param_name: Optional[str] = None
|
| 27 |
+
http_connection_param_name: Optional[str] = None
|
| 28 |
+
response_param_name: Optional[str] = None
|
| 29 |
+
background_tasks_param_name: Optional[str] = None
|
| 30 |
+
security_scopes_param_name: Optional[str] = None
|
| 31 |
+
security_scopes: Optional[List[str]] = None
|
| 32 |
+
use_cache: bool = True
|
| 33 |
+
path: Optional[str] = None
|
| 34 |
+
cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False)
|
| 35 |
+
|
| 36 |
+
def __post_init__(self) -> None:
|
| 37 |
+
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))
|
.venv/lib/python3.11/site-packages/fastapi/dependencies/utils.py
ADDED
|
@@ -0,0 +1,972 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
from contextlib import AsyncExitStack, contextmanager
|
| 3 |
+
from copy import copy, deepcopy
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import (
|
| 6 |
+
Any,
|
| 7 |
+
Callable,
|
| 8 |
+
Coroutine,
|
| 9 |
+
Dict,
|
| 10 |
+
ForwardRef,
|
| 11 |
+
List,
|
| 12 |
+
Mapping,
|
| 13 |
+
Optional,
|
| 14 |
+
Sequence,
|
| 15 |
+
Tuple,
|
| 16 |
+
Type,
|
| 17 |
+
Union,
|
| 18 |
+
cast,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
import anyio
|
| 22 |
+
from fastapi import params
|
| 23 |
+
from fastapi._compat import (
|
| 24 |
+
PYDANTIC_V2,
|
| 25 |
+
ErrorWrapper,
|
| 26 |
+
ModelField,
|
| 27 |
+
RequiredParam,
|
| 28 |
+
Undefined,
|
| 29 |
+
_regenerate_error_with_loc,
|
| 30 |
+
copy_field_info,
|
| 31 |
+
create_body_model,
|
| 32 |
+
evaluate_forwardref,
|
| 33 |
+
field_annotation_is_scalar,
|
| 34 |
+
get_annotation_from_field_info,
|
| 35 |
+
get_cached_model_fields,
|
| 36 |
+
get_missing_field_error,
|
| 37 |
+
is_bytes_field,
|
| 38 |
+
is_bytes_sequence_field,
|
| 39 |
+
is_scalar_field,
|
| 40 |
+
is_scalar_sequence_field,
|
| 41 |
+
is_sequence_field,
|
| 42 |
+
is_uploadfile_or_nonable_uploadfile_annotation,
|
| 43 |
+
is_uploadfile_sequence_annotation,
|
| 44 |
+
lenient_issubclass,
|
| 45 |
+
sequence_types,
|
| 46 |
+
serialize_sequence_value,
|
| 47 |
+
value_is_sequence,
|
| 48 |
+
)
|
| 49 |
+
from fastapi.background import BackgroundTasks
|
| 50 |
+
from fastapi.concurrency import (
|
| 51 |
+
asynccontextmanager,
|
| 52 |
+
contextmanager_in_threadpool,
|
| 53 |
+
)
|
| 54 |
+
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
| 55 |
+
from fastapi.logger import logger
|
| 56 |
+
from fastapi.security.base import SecurityBase
|
| 57 |
+
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
| 58 |
+
from fastapi.security.open_id_connect_url import OpenIdConnect
|
| 59 |
+
from fastapi.utils import create_model_field, get_path_param_names
|
| 60 |
+
from pydantic import BaseModel
|
| 61 |
+
from pydantic.fields import FieldInfo
|
| 62 |
+
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
| 63 |
+
from starlette.concurrency import run_in_threadpool
|
| 64 |
+
from starlette.datastructures import (
|
| 65 |
+
FormData,
|
| 66 |
+
Headers,
|
| 67 |
+
ImmutableMultiDict,
|
| 68 |
+
QueryParams,
|
| 69 |
+
UploadFile,
|
| 70 |
+
)
|
| 71 |
+
from starlette.requests import HTTPConnection, Request
|
| 72 |
+
from starlette.responses import Response
|
| 73 |
+
from starlette.websockets import WebSocket
|
| 74 |
+
from typing_extensions import Annotated, get_args, get_origin
|
| 75 |
+
|
| 76 |
+
multipart_not_installed_error = (
|
| 77 |
+
'Form data requires "python-multipart" to be installed. \n'
|
| 78 |
+
'You can install "python-multipart" with: \n\n'
|
| 79 |
+
"pip install python-multipart\n"
|
| 80 |
+
)
|
| 81 |
+
multipart_incorrect_install_error = (
|
| 82 |
+
'Form data requires "python-multipart" to be installed. '
|
| 83 |
+
'It seems you installed "multipart" instead. \n'
|
| 84 |
+
'You can remove "multipart" with: \n\n'
|
| 85 |
+
"pip uninstall multipart\n\n"
|
| 86 |
+
'And then install "python-multipart" with: \n\n'
|
| 87 |
+
"pip install python-multipart\n"
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def ensure_multipart_is_installed() -> None:
|
| 92 |
+
try:
|
| 93 |
+
from python_multipart import __version__
|
| 94 |
+
|
| 95 |
+
# Import an attribute that can be mocked/deleted in testing
|
| 96 |
+
assert __version__ > "0.0.12"
|
| 97 |
+
except (ImportError, AssertionError):
|
| 98 |
+
try:
|
| 99 |
+
# __version__ is available in both multiparts, and can be mocked
|
| 100 |
+
from multipart import __version__ # type: ignore[no-redef,import-untyped]
|
| 101 |
+
|
| 102 |
+
assert __version__
|
| 103 |
+
try:
|
| 104 |
+
# parse_options_header is only available in the right multipart
|
| 105 |
+
from multipart.multipart import ( # type: ignore[import-untyped]
|
| 106 |
+
parse_options_header,
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
assert parse_options_header
|
| 110 |
+
except ImportError:
|
| 111 |
+
logger.error(multipart_incorrect_install_error)
|
| 112 |
+
raise RuntimeError(multipart_incorrect_install_error) from None
|
| 113 |
+
except ImportError:
|
| 114 |
+
logger.error(multipart_not_installed_error)
|
| 115 |
+
raise RuntimeError(multipart_not_installed_error) from None
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_param_sub_dependant(
|
| 119 |
+
*,
|
| 120 |
+
param_name: str,
|
| 121 |
+
depends: params.Depends,
|
| 122 |
+
path: str,
|
| 123 |
+
security_scopes: Optional[List[str]] = None,
|
| 124 |
+
) -> Dependant:
|
| 125 |
+
assert depends.dependency
|
| 126 |
+
return get_sub_dependant(
|
| 127 |
+
depends=depends,
|
| 128 |
+
dependency=depends.dependency,
|
| 129 |
+
path=path,
|
| 130 |
+
name=param_name,
|
| 131 |
+
security_scopes=security_scopes,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:
|
| 136 |
+
assert callable(
|
| 137 |
+
depends.dependency
|
| 138 |
+
), "A parameter-less dependency must have a callable dependency"
|
| 139 |
+
return get_sub_dependant(depends=depends, dependency=depends.dependency, path=path)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def get_sub_dependant(
|
| 143 |
+
*,
|
| 144 |
+
depends: params.Depends,
|
| 145 |
+
dependency: Callable[..., Any],
|
| 146 |
+
path: str,
|
| 147 |
+
name: Optional[str] = None,
|
| 148 |
+
security_scopes: Optional[List[str]] = None,
|
| 149 |
+
) -> Dependant:
|
| 150 |
+
security_requirement = None
|
| 151 |
+
security_scopes = security_scopes or []
|
| 152 |
+
if isinstance(depends, params.Security):
|
| 153 |
+
dependency_scopes = depends.scopes
|
| 154 |
+
security_scopes.extend(dependency_scopes)
|
| 155 |
+
if isinstance(dependency, SecurityBase):
|
| 156 |
+
use_scopes: List[str] = []
|
| 157 |
+
if isinstance(dependency, (OAuth2, OpenIdConnect)):
|
| 158 |
+
use_scopes = security_scopes
|
| 159 |
+
security_requirement = SecurityRequirement(
|
| 160 |
+
security_scheme=dependency, scopes=use_scopes
|
| 161 |
+
)
|
| 162 |
+
sub_dependant = get_dependant(
|
| 163 |
+
path=path,
|
| 164 |
+
call=dependency,
|
| 165 |
+
name=name,
|
| 166 |
+
security_scopes=security_scopes,
|
| 167 |
+
use_cache=depends.use_cache,
|
| 168 |
+
)
|
| 169 |
+
if security_requirement:
|
| 170 |
+
sub_dependant.security_requirements.append(security_requirement)
|
| 171 |
+
return sub_dependant
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
CacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...]]
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def get_flat_dependant(
|
| 178 |
+
dependant: Dependant,
|
| 179 |
+
*,
|
| 180 |
+
skip_repeats: bool = False,
|
| 181 |
+
visited: Optional[List[CacheKey]] = None,
|
| 182 |
+
) -> Dependant:
|
| 183 |
+
if visited is None:
|
| 184 |
+
visited = []
|
| 185 |
+
visited.append(dependant.cache_key)
|
| 186 |
+
|
| 187 |
+
flat_dependant = Dependant(
|
| 188 |
+
path_params=dependant.path_params.copy(),
|
| 189 |
+
query_params=dependant.query_params.copy(),
|
| 190 |
+
header_params=dependant.header_params.copy(),
|
| 191 |
+
cookie_params=dependant.cookie_params.copy(),
|
| 192 |
+
body_params=dependant.body_params.copy(),
|
| 193 |
+
security_requirements=dependant.security_requirements.copy(),
|
| 194 |
+
use_cache=dependant.use_cache,
|
| 195 |
+
path=dependant.path,
|
| 196 |
+
)
|
| 197 |
+
for sub_dependant in dependant.dependencies:
|
| 198 |
+
if skip_repeats and sub_dependant.cache_key in visited:
|
| 199 |
+
continue
|
| 200 |
+
flat_sub = get_flat_dependant(
|
| 201 |
+
sub_dependant, skip_repeats=skip_repeats, visited=visited
|
| 202 |
+
)
|
| 203 |
+
flat_dependant.path_params.extend(flat_sub.path_params)
|
| 204 |
+
flat_dependant.query_params.extend(flat_sub.query_params)
|
| 205 |
+
flat_dependant.header_params.extend(flat_sub.header_params)
|
| 206 |
+
flat_dependant.cookie_params.extend(flat_sub.cookie_params)
|
| 207 |
+
flat_dependant.body_params.extend(flat_sub.body_params)
|
| 208 |
+
flat_dependant.security_requirements.extend(flat_sub.security_requirements)
|
| 209 |
+
return flat_dependant
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]:
|
| 213 |
+
if not fields:
|
| 214 |
+
return fields
|
| 215 |
+
first_field = fields[0]
|
| 216 |
+
if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
|
| 217 |
+
fields_to_extract = get_cached_model_fields(first_field.type_)
|
| 218 |
+
return fields_to_extract
|
| 219 |
+
return fields
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def get_flat_params(dependant: Dependant) -> List[ModelField]:
|
| 223 |
+
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
| 224 |
+
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
|
| 225 |
+
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
|
| 226 |
+
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
|
| 227 |
+
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
|
| 228 |
+
return path_params + query_params + header_params + cookie_params
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
| 232 |
+
signature = inspect.signature(call)
|
| 233 |
+
globalns = getattr(call, "__globals__", {})
|
| 234 |
+
typed_params = [
|
| 235 |
+
inspect.Parameter(
|
| 236 |
+
name=param.name,
|
| 237 |
+
kind=param.kind,
|
| 238 |
+
default=param.default,
|
| 239 |
+
annotation=get_typed_annotation(param.annotation, globalns),
|
| 240 |
+
)
|
| 241 |
+
for param in signature.parameters.values()
|
| 242 |
+
]
|
| 243 |
+
typed_signature = inspect.Signature(typed_params)
|
| 244 |
+
return typed_signature
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
|
| 248 |
+
if isinstance(annotation, str):
|
| 249 |
+
annotation = ForwardRef(annotation)
|
| 250 |
+
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
| 251 |
+
return annotation
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
|
| 255 |
+
signature = inspect.signature(call)
|
| 256 |
+
annotation = signature.return_annotation
|
| 257 |
+
|
| 258 |
+
if annotation is inspect.Signature.empty:
|
| 259 |
+
return None
|
| 260 |
+
|
| 261 |
+
globalns = getattr(call, "__globals__", {})
|
| 262 |
+
return get_typed_annotation(annotation, globalns)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def get_dependant(
|
| 266 |
+
*,
|
| 267 |
+
path: str,
|
| 268 |
+
call: Callable[..., Any],
|
| 269 |
+
name: Optional[str] = None,
|
| 270 |
+
security_scopes: Optional[List[str]] = None,
|
| 271 |
+
use_cache: bool = True,
|
| 272 |
+
) -> Dependant:
|
| 273 |
+
path_param_names = get_path_param_names(path)
|
| 274 |
+
endpoint_signature = get_typed_signature(call)
|
| 275 |
+
signature_params = endpoint_signature.parameters
|
| 276 |
+
dependant = Dependant(
|
| 277 |
+
call=call,
|
| 278 |
+
name=name,
|
| 279 |
+
path=path,
|
| 280 |
+
security_scopes=security_scopes,
|
| 281 |
+
use_cache=use_cache,
|
| 282 |
+
)
|
| 283 |
+
for param_name, param in signature_params.items():
|
| 284 |
+
is_path_param = param_name in path_param_names
|
| 285 |
+
param_details = analyze_param(
|
| 286 |
+
param_name=param_name,
|
| 287 |
+
annotation=param.annotation,
|
| 288 |
+
value=param.default,
|
| 289 |
+
is_path_param=is_path_param,
|
| 290 |
+
)
|
| 291 |
+
if param_details.depends is not None:
|
| 292 |
+
sub_dependant = get_param_sub_dependant(
|
| 293 |
+
param_name=param_name,
|
| 294 |
+
depends=param_details.depends,
|
| 295 |
+
path=path,
|
| 296 |
+
security_scopes=security_scopes,
|
| 297 |
+
)
|
| 298 |
+
dependant.dependencies.append(sub_dependant)
|
| 299 |
+
continue
|
| 300 |
+
if add_non_field_param_to_dependency(
|
| 301 |
+
param_name=param_name,
|
| 302 |
+
type_annotation=param_details.type_annotation,
|
| 303 |
+
dependant=dependant,
|
| 304 |
+
):
|
| 305 |
+
assert (
|
| 306 |
+
param_details.field is None
|
| 307 |
+
), f"Cannot specify multiple FastAPI annotations for {param_name!r}"
|
| 308 |
+
continue
|
| 309 |
+
assert param_details.field is not None
|
| 310 |
+
if isinstance(param_details.field.field_info, params.Body):
|
| 311 |
+
dependant.body_params.append(param_details.field)
|
| 312 |
+
else:
|
| 313 |
+
add_param_to_fields(field=param_details.field, dependant=dependant)
|
| 314 |
+
return dependant
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def add_non_field_param_to_dependency(
|
| 318 |
+
*, param_name: str, type_annotation: Any, dependant: Dependant
|
| 319 |
+
) -> Optional[bool]:
|
| 320 |
+
if lenient_issubclass(type_annotation, Request):
|
| 321 |
+
dependant.request_param_name = param_name
|
| 322 |
+
return True
|
| 323 |
+
elif lenient_issubclass(type_annotation, WebSocket):
|
| 324 |
+
dependant.websocket_param_name = param_name
|
| 325 |
+
return True
|
| 326 |
+
elif lenient_issubclass(type_annotation, HTTPConnection):
|
| 327 |
+
dependant.http_connection_param_name = param_name
|
| 328 |
+
return True
|
| 329 |
+
elif lenient_issubclass(type_annotation, Response):
|
| 330 |
+
dependant.response_param_name = param_name
|
| 331 |
+
return True
|
| 332 |
+
elif lenient_issubclass(type_annotation, StarletteBackgroundTasks):
|
| 333 |
+
dependant.background_tasks_param_name = param_name
|
| 334 |
+
return True
|
| 335 |
+
elif lenient_issubclass(type_annotation, SecurityScopes):
|
| 336 |
+
dependant.security_scopes_param_name = param_name
|
| 337 |
+
return True
|
| 338 |
+
return None
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
@dataclass
|
| 342 |
+
class ParamDetails:
|
| 343 |
+
type_annotation: Any
|
| 344 |
+
depends: Optional[params.Depends]
|
| 345 |
+
field: Optional[ModelField]
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def analyze_param(
|
| 349 |
+
*,
|
| 350 |
+
param_name: str,
|
| 351 |
+
annotation: Any,
|
| 352 |
+
value: Any,
|
| 353 |
+
is_path_param: bool,
|
| 354 |
+
) -> ParamDetails:
|
| 355 |
+
field_info = None
|
| 356 |
+
depends = None
|
| 357 |
+
type_annotation: Any = Any
|
| 358 |
+
use_annotation: Any = Any
|
| 359 |
+
if annotation is not inspect.Signature.empty:
|
| 360 |
+
use_annotation = annotation
|
| 361 |
+
type_annotation = annotation
|
| 362 |
+
# Extract Annotated info
|
| 363 |
+
if get_origin(use_annotation) is Annotated:
|
| 364 |
+
annotated_args = get_args(annotation)
|
| 365 |
+
type_annotation = annotated_args[0]
|
| 366 |
+
fastapi_annotations = [
|
| 367 |
+
arg
|
| 368 |
+
for arg in annotated_args[1:]
|
| 369 |
+
if isinstance(arg, (FieldInfo, params.Depends))
|
| 370 |
+
]
|
| 371 |
+
fastapi_specific_annotations = [
|
| 372 |
+
arg
|
| 373 |
+
for arg in fastapi_annotations
|
| 374 |
+
if isinstance(arg, (params.Param, params.Body, params.Depends))
|
| 375 |
+
]
|
| 376 |
+
if fastapi_specific_annotations:
|
| 377 |
+
fastapi_annotation: Union[FieldInfo, params.Depends, None] = (
|
| 378 |
+
fastapi_specific_annotations[-1]
|
| 379 |
+
)
|
| 380 |
+
else:
|
| 381 |
+
fastapi_annotation = None
|
| 382 |
+
# Set default for Annotated FieldInfo
|
| 383 |
+
if isinstance(fastapi_annotation, FieldInfo):
|
| 384 |
+
# Copy `field_info` because we mutate `field_info.default` below.
|
| 385 |
+
field_info = copy_field_info(
|
| 386 |
+
field_info=fastapi_annotation, annotation=use_annotation
|
| 387 |
+
)
|
| 388 |
+
assert (
|
| 389 |
+
field_info.default is Undefined or field_info.default is RequiredParam
|
| 390 |
+
), (
|
| 391 |
+
f"`{field_info.__class__.__name__}` default value cannot be set in"
|
| 392 |
+
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
|
| 393 |
+
)
|
| 394 |
+
if value is not inspect.Signature.empty:
|
| 395 |
+
assert not is_path_param, "Path parameters cannot have default values"
|
| 396 |
+
field_info.default = value
|
| 397 |
+
else:
|
| 398 |
+
field_info.default = RequiredParam
|
| 399 |
+
# Get Annotated Depends
|
| 400 |
+
elif isinstance(fastapi_annotation, params.Depends):
|
| 401 |
+
depends = fastapi_annotation
|
| 402 |
+
# Get Depends from default value
|
| 403 |
+
if isinstance(value, params.Depends):
|
| 404 |
+
assert depends is None, (
|
| 405 |
+
"Cannot specify `Depends` in `Annotated` and default value"
|
| 406 |
+
f" together for {param_name!r}"
|
| 407 |
+
)
|
| 408 |
+
assert field_info is None, (
|
| 409 |
+
"Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a"
|
| 410 |
+
f" default value together for {param_name!r}"
|
| 411 |
+
)
|
| 412 |
+
depends = value
|
| 413 |
+
# Get FieldInfo from default value
|
| 414 |
+
elif isinstance(value, FieldInfo):
|
| 415 |
+
assert field_info is None, (
|
| 416 |
+
"Cannot specify FastAPI annotations in `Annotated` and default value"
|
| 417 |
+
f" together for {param_name!r}"
|
| 418 |
+
)
|
| 419 |
+
field_info = value
|
| 420 |
+
if PYDANTIC_V2:
|
| 421 |
+
field_info.annotation = type_annotation
|
| 422 |
+
|
| 423 |
+
# Get Depends from type annotation
|
| 424 |
+
if depends is not None and depends.dependency is None:
|
| 425 |
+
# Copy `depends` before mutating it
|
| 426 |
+
depends = copy(depends)
|
| 427 |
+
depends.dependency = type_annotation
|
| 428 |
+
|
| 429 |
+
# Handle non-param type annotations like Request
|
| 430 |
+
if lenient_issubclass(
|
| 431 |
+
type_annotation,
|
| 432 |
+
(
|
| 433 |
+
Request,
|
| 434 |
+
WebSocket,
|
| 435 |
+
HTTPConnection,
|
| 436 |
+
Response,
|
| 437 |
+
StarletteBackgroundTasks,
|
| 438 |
+
SecurityScopes,
|
| 439 |
+
),
|
| 440 |
+
):
|
| 441 |
+
assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}"
|
| 442 |
+
assert (
|
| 443 |
+
field_info is None
|
| 444 |
+
), f"Cannot specify FastAPI annotation for type {type_annotation!r}"
|
| 445 |
+
# Handle default assignations, neither field_info nor depends was not found in Annotated nor default value
|
| 446 |
+
elif field_info is None and depends is None:
|
| 447 |
+
default_value = value if value is not inspect.Signature.empty else RequiredParam
|
| 448 |
+
if is_path_param:
|
| 449 |
+
# We might check here that `default_value is RequiredParam`, but the fact is that the same
|
| 450 |
+
# parameter might sometimes be a path parameter and sometimes not. See
|
| 451 |
+
# `tests/test_infer_param_optionality.py` for an example.
|
| 452 |
+
field_info = params.Path(annotation=use_annotation)
|
| 453 |
+
elif is_uploadfile_or_nonable_uploadfile_annotation(
|
| 454 |
+
type_annotation
|
| 455 |
+
) or is_uploadfile_sequence_annotation(type_annotation):
|
| 456 |
+
field_info = params.File(annotation=use_annotation, default=default_value)
|
| 457 |
+
elif not field_annotation_is_scalar(annotation=type_annotation):
|
| 458 |
+
field_info = params.Body(annotation=use_annotation, default=default_value)
|
| 459 |
+
else:
|
| 460 |
+
field_info = params.Query(annotation=use_annotation, default=default_value)
|
| 461 |
+
|
| 462 |
+
field = None
|
| 463 |
+
# It's a field_info, not a dependency
|
| 464 |
+
if field_info is not None:
|
| 465 |
+
# Handle field_info.in_
|
| 466 |
+
if is_path_param:
|
| 467 |
+
assert isinstance(field_info, params.Path), (
|
| 468 |
+
f"Cannot use `{field_info.__class__.__name__}` for path param"
|
| 469 |
+
f" {param_name!r}"
|
| 470 |
+
)
|
| 471 |
+
elif (
|
| 472 |
+
isinstance(field_info, params.Param)
|
| 473 |
+
and getattr(field_info, "in_", None) is None
|
| 474 |
+
):
|
| 475 |
+
field_info.in_ = params.ParamTypes.query
|
| 476 |
+
use_annotation_from_field_info = get_annotation_from_field_info(
|
| 477 |
+
use_annotation,
|
| 478 |
+
field_info,
|
| 479 |
+
param_name,
|
| 480 |
+
)
|
| 481 |
+
if isinstance(field_info, params.Form):
|
| 482 |
+
ensure_multipart_is_installed()
|
| 483 |
+
if not field_info.alias and getattr(field_info, "convert_underscores", None):
|
| 484 |
+
alias = param_name.replace("_", "-")
|
| 485 |
+
else:
|
| 486 |
+
alias = field_info.alias or param_name
|
| 487 |
+
field_info.alias = alias
|
| 488 |
+
field = create_model_field(
|
| 489 |
+
name=param_name,
|
| 490 |
+
type_=use_annotation_from_field_info,
|
| 491 |
+
default=field_info.default,
|
| 492 |
+
alias=alias,
|
| 493 |
+
required=field_info.default in (RequiredParam, Undefined),
|
| 494 |
+
field_info=field_info,
|
| 495 |
+
)
|
| 496 |
+
if is_path_param:
|
| 497 |
+
assert is_scalar_field(
|
| 498 |
+
field=field
|
| 499 |
+
), "Path params must be of one of the supported types"
|
| 500 |
+
elif isinstance(field_info, params.Query):
|
| 501 |
+
assert (
|
| 502 |
+
is_scalar_field(field)
|
| 503 |
+
or is_scalar_sequence_field(field)
|
| 504 |
+
or (
|
| 505 |
+
lenient_issubclass(field.type_, BaseModel)
|
| 506 |
+
# For Pydantic v1
|
| 507 |
+
and getattr(field, "shape", 1) == 1
|
| 508 |
+
)
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
|
| 515 |
+
field_info = field.field_info
|
| 516 |
+
field_info_in = getattr(field_info, "in_", None)
|
| 517 |
+
if field_info_in == params.ParamTypes.path:
|
| 518 |
+
dependant.path_params.append(field)
|
| 519 |
+
elif field_info_in == params.ParamTypes.query:
|
| 520 |
+
dependant.query_params.append(field)
|
| 521 |
+
elif field_info_in == params.ParamTypes.header:
|
| 522 |
+
dependant.header_params.append(field)
|
| 523 |
+
else:
|
| 524 |
+
assert (
|
| 525 |
+
field_info_in == params.ParamTypes.cookie
|
| 526 |
+
), f"non-body parameters must be in path, query, header or cookie: {field.name}"
|
| 527 |
+
dependant.cookie_params.append(field)
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
| 531 |
+
if inspect.isroutine(call):
|
| 532 |
+
return inspect.iscoroutinefunction(call)
|
| 533 |
+
if inspect.isclass(call):
|
| 534 |
+
return False
|
| 535 |
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
| 536 |
+
return inspect.iscoroutinefunction(dunder_call)
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def is_async_gen_callable(call: Callable[..., Any]) -> bool:
|
| 540 |
+
if inspect.isasyncgenfunction(call):
|
| 541 |
+
return True
|
| 542 |
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
| 543 |
+
return inspect.isasyncgenfunction(dunder_call)
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
def is_gen_callable(call: Callable[..., Any]) -> bool:
|
| 547 |
+
if inspect.isgeneratorfunction(call):
|
| 548 |
+
return True
|
| 549 |
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
| 550 |
+
return inspect.isgeneratorfunction(dunder_call)
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
async def solve_generator(
|
| 554 |
+
*, call: Callable[..., Any], stack: AsyncExitStack, sub_values: Dict[str, Any]
|
| 555 |
+
) -> Any:
|
| 556 |
+
if is_gen_callable(call):
|
| 557 |
+
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
|
| 558 |
+
elif is_async_gen_callable(call):
|
| 559 |
+
cm = asynccontextmanager(call)(**sub_values)
|
| 560 |
+
return await stack.enter_async_context(cm)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
@dataclass
|
| 564 |
+
class SolvedDependency:
|
| 565 |
+
values: Dict[str, Any]
|
| 566 |
+
errors: List[Any]
|
| 567 |
+
background_tasks: Optional[StarletteBackgroundTasks]
|
| 568 |
+
response: Response
|
| 569 |
+
dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any]
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
async def solve_dependencies(
|
| 573 |
+
*,
|
| 574 |
+
request: Union[Request, WebSocket],
|
| 575 |
+
dependant: Dependant,
|
| 576 |
+
body: Optional[Union[Dict[str, Any], FormData]] = None,
|
| 577 |
+
background_tasks: Optional[StarletteBackgroundTasks] = None,
|
| 578 |
+
response: Optional[Response] = None,
|
| 579 |
+
dependency_overrides_provider: Optional[Any] = None,
|
| 580 |
+
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
| 581 |
+
async_exit_stack: AsyncExitStack,
|
| 582 |
+
embed_body_fields: bool,
|
| 583 |
+
) -> SolvedDependency:
|
| 584 |
+
values: Dict[str, Any] = {}
|
| 585 |
+
errors: List[Any] = []
|
| 586 |
+
if response is None:
|
| 587 |
+
response = Response()
|
| 588 |
+
del response.headers["content-length"]
|
| 589 |
+
response.status_code = None # type: ignore
|
| 590 |
+
dependency_cache = dependency_cache or {}
|
| 591 |
+
sub_dependant: Dependant
|
| 592 |
+
for sub_dependant in dependant.dependencies:
|
| 593 |
+
sub_dependant.call = cast(Callable[..., Any], sub_dependant.call)
|
| 594 |
+
sub_dependant.cache_key = cast(
|
| 595 |
+
Tuple[Callable[..., Any], Tuple[str]], sub_dependant.cache_key
|
| 596 |
+
)
|
| 597 |
+
call = sub_dependant.call
|
| 598 |
+
use_sub_dependant = sub_dependant
|
| 599 |
+
if (
|
| 600 |
+
dependency_overrides_provider
|
| 601 |
+
and dependency_overrides_provider.dependency_overrides
|
| 602 |
+
):
|
| 603 |
+
original_call = sub_dependant.call
|
| 604 |
+
call = getattr(
|
| 605 |
+
dependency_overrides_provider, "dependency_overrides", {}
|
| 606 |
+
).get(original_call, original_call)
|
| 607 |
+
use_path: str = sub_dependant.path # type: ignore
|
| 608 |
+
use_sub_dependant = get_dependant(
|
| 609 |
+
path=use_path,
|
| 610 |
+
call=call,
|
| 611 |
+
name=sub_dependant.name,
|
| 612 |
+
security_scopes=sub_dependant.security_scopes,
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
solved_result = await solve_dependencies(
|
| 616 |
+
request=request,
|
| 617 |
+
dependant=use_sub_dependant,
|
| 618 |
+
body=body,
|
| 619 |
+
background_tasks=background_tasks,
|
| 620 |
+
response=response,
|
| 621 |
+
dependency_overrides_provider=dependency_overrides_provider,
|
| 622 |
+
dependency_cache=dependency_cache,
|
| 623 |
+
async_exit_stack=async_exit_stack,
|
| 624 |
+
embed_body_fields=embed_body_fields,
|
| 625 |
+
)
|
| 626 |
+
background_tasks = solved_result.background_tasks
|
| 627 |
+
dependency_cache.update(solved_result.dependency_cache)
|
| 628 |
+
if solved_result.errors:
|
| 629 |
+
errors.extend(solved_result.errors)
|
| 630 |
+
continue
|
| 631 |
+
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
|
| 632 |
+
solved = dependency_cache[sub_dependant.cache_key]
|
| 633 |
+
elif is_gen_callable(call) or is_async_gen_callable(call):
|
| 634 |
+
solved = await solve_generator(
|
| 635 |
+
call=call, stack=async_exit_stack, sub_values=solved_result.values
|
| 636 |
+
)
|
| 637 |
+
elif is_coroutine_callable(call):
|
| 638 |
+
solved = await call(**solved_result.values)
|
| 639 |
+
else:
|
| 640 |
+
solved = await run_in_threadpool(call, **solved_result.values)
|
| 641 |
+
if sub_dependant.name is not None:
|
| 642 |
+
values[sub_dependant.name] = solved
|
| 643 |
+
if sub_dependant.cache_key not in dependency_cache:
|
| 644 |
+
dependency_cache[sub_dependant.cache_key] = solved
|
| 645 |
+
path_values, path_errors = request_params_to_args(
|
| 646 |
+
dependant.path_params, request.path_params
|
| 647 |
+
)
|
| 648 |
+
query_values, query_errors = request_params_to_args(
|
| 649 |
+
dependant.query_params, request.query_params
|
| 650 |
+
)
|
| 651 |
+
header_values, header_errors = request_params_to_args(
|
| 652 |
+
dependant.header_params, request.headers
|
| 653 |
+
)
|
| 654 |
+
cookie_values, cookie_errors = request_params_to_args(
|
| 655 |
+
dependant.cookie_params, request.cookies
|
| 656 |
+
)
|
| 657 |
+
values.update(path_values)
|
| 658 |
+
values.update(query_values)
|
| 659 |
+
values.update(header_values)
|
| 660 |
+
values.update(cookie_values)
|
| 661 |
+
errors += path_errors + query_errors + header_errors + cookie_errors
|
| 662 |
+
if dependant.body_params:
|
| 663 |
+
(
|
| 664 |
+
body_values,
|
| 665 |
+
body_errors,
|
| 666 |
+
) = await request_body_to_args( # body_params checked above
|
| 667 |
+
body_fields=dependant.body_params,
|
| 668 |
+
received_body=body,
|
| 669 |
+
embed_body_fields=embed_body_fields,
|
| 670 |
+
)
|
| 671 |
+
values.update(body_values)
|
| 672 |
+
errors.extend(body_errors)
|
| 673 |
+
if dependant.http_connection_param_name:
|
| 674 |
+
values[dependant.http_connection_param_name] = request
|
| 675 |
+
if dependant.request_param_name and isinstance(request, Request):
|
| 676 |
+
values[dependant.request_param_name] = request
|
| 677 |
+
elif dependant.websocket_param_name and isinstance(request, WebSocket):
|
| 678 |
+
values[dependant.websocket_param_name] = request
|
| 679 |
+
if dependant.background_tasks_param_name:
|
| 680 |
+
if background_tasks is None:
|
| 681 |
+
background_tasks = BackgroundTasks()
|
| 682 |
+
values[dependant.background_tasks_param_name] = background_tasks
|
| 683 |
+
if dependant.response_param_name:
|
| 684 |
+
values[dependant.response_param_name] = response
|
| 685 |
+
if dependant.security_scopes_param_name:
|
| 686 |
+
values[dependant.security_scopes_param_name] = SecurityScopes(
|
| 687 |
+
scopes=dependant.security_scopes
|
| 688 |
+
)
|
| 689 |
+
return SolvedDependency(
|
| 690 |
+
values=values,
|
| 691 |
+
errors=errors,
|
| 692 |
+
background_tasks=background_tasks,
|
| 693 |
+
response=response,
|
| 694 |
+
dependency_cache=dependency_cache,
|
| 695 |
+
)
|
| 696 |
+
|
| 697 |
+
|
| 698 |
+
def _validate_value_with_model_field(
|
| 699 |
+
*, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...]
|
| 700 |
+
) -> Tuple[Any, List[Any]]:
|
| 701 |
+
if value is None:
|
| 702 |
+
if field.required:
|
| 703 |
+
return None, [get_missing_field_error(loc=loc)]
|
| 704 |
+
else:
|
| 705 |
+
return deepcopy(field.default), []
|
| 706 |
+
v_, errors_ = field.validate(value, values, loc=loc)
|
| 707 |
+
if isinstance(errors_, ErrorWrapper):
|
| 708 |
+
return None, [errors_]
|
| 709 |
+
elif isinstance(errors_, list):
|
| 710 |
+
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
| 711 |
+
return None, new_errors
|
| 712 |
+
else:
|
| 713 |
+
return v_, []
|
| 714 |
+
|
| 715 |
+
|
| 716 |
+
def _get_multidict_value(
|
| 717 |
+
field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None
|
| 718 |
+
) -> Any:
|
| 719 |
+
alias = alias or field.alias
|
| 720 |
+
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
|
| 721 |
+
value = values.getlist(alias)
|
| 722 |
+
else:
|
| 723 |
+
value = values.get(alias, None)
|
| 724 |
+
if (
|
| 725 |
+
value is None
|
| 726 |
+
or (
|
| 727 |
+
isinstance(field.field_info, params.Form)
|
| 728 |
+
and isinstance(value, str) # For type checks
|
| 729 |
+
and value == ""
|
| 730 |
+
)
|
| 731 |
+
or (is_sequence_field(field) and len(value) == 0)
|
| 732 |
+
):
|
| 733 |
+
if field.required:
|
| 734 |
+
return
|
| 735 |
+
else:
|
| 736 |
+
return deepcopy(field.default)
|
| 737 |
+
return value
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
def request_params_to_args(
|
| 741 |
+
fields: Sequence[ModelField],
|
| 742 |
+
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
| 743 |
+
) -> Tuple[Dict[str, Any], List[Any]]:
|
| 744 |
+
values: Dict[str, Any] = {}
|
| 745 |
+
errors: List[Dict[str, Any]] = []
|
| 746 |
+
|
| 747 |
+
if not fields:
|
| 748 |
+
return values, errors
|
| 749 |
+
|
| 750 |
+
first_field = fields[0]
|
| 751 |
+
fields_to_extract = fields
|
| 752 |
+
single_not_embedded_field = False
|
| 753 |
+
if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
|
| 754 |
+
fields_to_extract = get_cached_model_fields(first_field.type_)
|
| 755 |
+
single_not_embedded_field = True
|
| 756 |
+
|
| 757 |
+
params_to_process: Dict[str, Any] = {}
|
| 758 |
+
|
| 759 |
+
processed_keys = set()
|
| 760 |
+
|
| 761 |
+
for field in fields_to_extract:
|
| 762 |
+
alias = None
|
| 763 |
+
if isinstance(received_params, Headers):
|
| 764 |
+
# Handle fields extracted from a Pydantic Model for a header, each field
|
| 765 |
+
# doesn't have a FieldInfo of type Header with the default convert_underscores=True
|
| 766 |
+
convert_underscores = getattr(field.field_info, "convert_underscores", True)
|
| 767 |
+
if convert_underscores:
|
| 768 |
+
alias = (
|
| 769 |
+
field.alias
|
| 770 |
+
if field.alias != field.name
|
| 771 |
+
else field.name.replace("_", "-")
|
| 772 |
+
)
|
| 773 |
+
value = _get_multidict_value(field, received_params, alias=alias)
|
| 774 |
+
if value is not None:
|
| 775 |
+
params_to_process[field.name] = value
|
| 776 |
+
processed_keys.add(alias or field.alias)
|
| 777 |
+
processed_keys.add(field.name)
|
| 778 |
+
|
| 779 |
+
for key, value in received_params.items():
|
| 780 |
+
if key not in processed_keys:
|
| 781 |
+
params_to_process[key] = value
|
| 782 |
+
|
| 783 |
+
if single_not_embedded_field:
|
| 784 |
+
field_info = first_field.field_info
|
| 785 |
+
assert isinstance(
|
| 786 |
+
field_info, params.Param
|
| 787 |
+
), "Params must be subclasses of Param"
|
| 788 |
+
loc: Tuple[str, ...] = (field_info.in_.value,)
|
| 789 |
+
v_, errors_ = _validate_value_with_model_field(
|
| 790 |
+
field=first_field, value=params_to_process, values=values, loc=loc
|
| 791 |
+
)
|
| 792 |
+
return {first_field.name: v_}, errors_
|
| 793 |
+
|
| 794 |
+
for field in fields:
|
| 795 |
+
value = _get_multidict_value(field, received_params)
|
| 796 |
+
field_info = field.field_info
|
| 797 |
+
assert isinstance(
|
| 798 |
+
field_info, params.Param
|
| 799 |
+
), "Params must be subclasses of Param"
|
| 800 |
+
loc = (field_info.in_.value, field.alias)
|
| 801 |
+
v_, errors_ = _validate_value_with_model_field(
|
| 802 |
+
field=field, value=value, values=values, loc=loc
|
| 803 |
+
)
|
| 804 |
+
if errors_:
|
| 805 |
+
errors.extend(errors_)
|
| 806 |
+
else:
|
| 807 |
+
values[field.name] = v_
|
| 808 |
+
return values, errors
|
| 809 |
+
|
| 810 |
+
|
| 811 |
+
def _should_embed_body_fields(fields: List[ModelField]) -> bool:
|
| 812 |
+
if not fields:
|
| 813 |
+
return False
|
| 814 |
+
# More than one dependency could have the same field, it would show up as multiple
|
| 815 |
+
# fields but it's the same one, so count them by name
|
| 816 |
+
body_param_names_set = {field.name for field in fields}
|
| 817 |
+
# A top level field has to be a single field, not multiple
|
| 818 |
+
if len(body_param_names_set) > 1:
|
| 819 |
+
return True
|
| 820 |
+
first_field = fields[0]
|
| 821 |
+
# If it explicitly specifies it is embedded, it has to be embedded
|
| 822 |
+
if getattr(first_field.field_info, "embed", None):
|
| 823 |
+
return True
|
| 824 |
+
# If it's a Form (or File) field, it has to be a BaseModel to be top level
|
| 825 |
+
# otherwise it has to be embedded, so that the key value pair can be extracted
|
| 826 |
+
if isinstance(first_field.field_info, params.Form) and not lenient_issubclass(
|
| 827 |
+
first_field.type_, BaseModel
|
| 828 |
+
):
|
| 829 |
+
return True
|
| 830 |
+
return False
|
| 831 |
+
|
| 832 |
+
|
| 833 |
+
async def _extract_form_body(
|
| 834 |
+
body_fields: List[ModelField],
|
| 835 |
+
received_body: FormData,
|
| 836 |
+
) -> Dict[str, Any]:
|
| 837 |
+
values = {}
|
| 838 |
+
first_field = body_fields[0]
|
| 839 |
+
first_field_info = first_field.field_info
|
| 840 |
+
|
| 841 |
+
for field in body_fields:
|
| 842 |
+
value = _get_multidict_value(field, received_body)
|
| 843 |
+
if (
|
| 844 |
+
isinstance(first_field_info, params.File)
|
| 845 |
+
and is_bytes_field(field)
|
| 846 |
+
and isinstance(value, UploadFile)
|
| 847 |
+
):
|
| 848 |
+
value = await value.read()
|
| 849 |
+
elif (
|
| 850 |
+
is_bytes_sequence_field(field)
|
| 851 |
+
and isinstance(first_field_info, params.File)
|
| 852 |
+
and value_is_sequence(value)
|
| 853 |
+
):
|
| 854 |
+
# For types
|
| 855 |
+
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
| 856 |
+
results: List[Union[bytes, str]] = []
|
| 857 |
+
|
| 858 |
+
async def process_fn(
|
| 859 |
+
fn: Callable[[], Coroutine[Any, Any, Any]],
|
| 860 |
+
) -> None:
|
| 861 |
+
result = await fn()
|
| 862 |
+
results.append(result) # noqa: B023
|
| 863 |
+
|
| 864 |
+
async with anyio.create_task_group() as tg:
|
| 865 |
+
for sub_value in value:
|
| 866 |
+
tg.start_soon(process_fn, sub_value.read)
|
| 867 |
+
value = serialize_sequence_value(field=field, value=results)
|
| 868 |
+
if value is not None:
|
| 869 |
+
values[field.alias] = value
|
| 870 |
+
for key, value in received_body.items():
|
| 871 |
+
if key not in values:
|
| 872 |
+
values[key] = value
|
| 873 |
+
return values
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
async def request_body_to_args(
|
| 877 |
+
body_fields: List[ModelField],
|
| 878 |
+
received_body: Optional[Union[Dict[str, Any], FormData]],
|
| 879 |
+
embed_body_fields: bool,
|
| 880 |
+
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
| 881 |
+
values: Dict[str, Any] = {}
|
| 882 |
+
errors: List[Dict[str, Any]] = []
|
| 883 |
+
assert body_fields, "request_body_to_args() should be called with fields"
|
| 884 |
+
single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields
|
| 885 |
+
first_field = body_fields[0]
|
| 886 |
+
body_to_process = received_body
|
| 887 |
+
|
| 888 |
+
fields_to_extract: List[ModelField] = body_fields
|
| 889 |
+
|
| 890 |
+
if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel):
|
| 891 |
+
fields_to_extract = get_cached_model_fields(first_field.type_)
|
| 892 |
+
|
| 893 |
+
if isinstance(received_body, FormData):
|
| 894 |
+
body_to_process = await _extract_form_body(fields_to_extract, received_body)
|
| 895 |
+
|
| 896 |
+
if single_not_embedded_field:
|
| 897 |
+
loc: Tuple[str, ...] = ("body",)
|
| 898 |
+
v_, errors_ = _validate_value_with_model_field(
|
| 899 |
+
field=first_field, value=body_to_process, values=values, loc=loc
|
| 900 |
+
)
|
| 901 |
+
return {first_field.name: v_}, errors_
|
| 902 |
+
for field in body_fields:
|
| 903 |
+
loc = ("body", field.alias)
|
| 904 |
+
value: Optional[Any] = None
|
| 905 |
+
if body_to_process is not None:
|
| 906 |
+
try:
|
| 907 |
+
value = body_to_process.get(field.alias)
|
| 908 |
+
# If the received body is a list, not a dict
|
| 909 |
+
except AttributeError:
|
| 910 |
+
errors.append(get_missing_field_error(loc))
|
| 911 |
+
continue
|
| 912 |
+
v_, errors_ = _validate_value_with_model_field(
|
| 913 |
+
field=field, value=value, values=values, loc=loc
|
| 914 |
+
)
|
| 915 |
+
if errors_:
|
| 916 |
+
errors.extend(errors_)
|
| 917 |
+
else:
|
| 918 |
+
values[field.name] = v_
|
| 919 |
+
return values, errors
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
def get_body_field(
|
| 923 |
+
*, flat_dependant: Dependant, name: str, embed_body_fields: bool
|
| 924 |
+
) -> Optional[ModelField]:
|
| 925 |
+
"""
|
| 926 |
+
Get a ModelField representing the request body for a path operation, combining
|
| 927 |
+
all body parameters into a single field if necessary.
|
| 928 |
+
|
| 929 |
+
Used to check if it's form data (with `isinstance(body_field, params.Form)`)
|
| 930 |
+
or JSON and to generate the JSON Schema for a request body.
|
| 931 |
+
|
| 932 |
+
This is **not** used to validate/parse the request body, that's done with each
|
| 933 |
+
individual body parameter.
|
| 934 |
+
"""
|
| 935 |
+
if not flat_dependant.body_params:
|
| 936 |
+
return None
|
| 937 |
+
first_param = flat_dependant.body_params[0]
|
| 938 |
+
if not embed_body_fields:
|
| 939 |
+
return first_param
|
| 940 |
+
model_name = "Body_" + name
|
| 941 |
+
BodyModel = create_body_model(
|
| 942 |
+
fields=flat_dependant.body_params, model_name=model_name
|
| 943 |
+
)
|
| 944 |
+
required = any(True for f in flat_dependant.body_params if f.required)
|
| 945 |
+
BodyFieldInfo_kwargs: Dict[str, Any] = {
|
| 946 |
+
"annotation": BodyModel,
|
| 947 |
+
"alias": "body",
|
| 948 |
+
}
|
| 949 |
+
if not required:
|
| 950 |
+
BodyFieldInfo_kwargs["default"] = None
|
| 951 |
+
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
|
| 952 |
+
BodyFieldInfo: Type[params.Body] = params.File
|
| 953 |
+
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
|
| 954 |
+
BodyFieldInfo = params.Form
|
| 955 |
+
else:
|
| 956 |
+
BodyFieldInfo = params.Body
|
| 957 |
+
|
| 958 |
+
body_param_media_types = [
|
| 959 |
+
f.field_info.media_type
|
| 960 |
+
for f in flat_dependant.body_params
|
| 961 |
+
if isinstance(f.field_info, params.Body)
|
| 962 |
+
]
|
| 963 |
+
if len(set(body_param_media_types)) == 1:
|
| 964 |
+
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
|
| 965 |
+
final_field = create_model_field(
|
| 966 |
+
name="body",
|
| 967 |
+
type_=BodyModel,
|
| 968 |
+
required=required,
|
| 969 |
+
alias="body",
|
| 970 |
+
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
|
| 971 |
+
)
|
| 972 |
+
return final_field
|
.venv/lib/python3.11/site-packages/fastapi/openapi/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (188 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/constants.cpython-311.pyc
ADDED
|
Binary file (358 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/docs.cpython-311.pyc
ADDED
|
Binary file (11.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (28.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/openapi/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (22.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/openapi/constants.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
|
| 2 |
+
REF_PREFIX = "#/components/schemas/"
|
| 3 |
+
REF_TEMPLATE = "#/components/schemas/{model}"
|
.venv/lib/python3.11/site-packages/fastapi/openapi/docs.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import Any, Dict, Optional
|
| 3 |
+
|
| 4 |
+
from fastapi.encoders import jsonable_encoder
|
| 5 |
+
from starlette.responses import HTMLResponse
|
| 6 |
+
from typing_extensions import Annotated, Doc
|
| 7 |
+
|
| 8 |
+
swagger_ui_default_parameters: Annotated[
|
| 9 |
+
Dict[str, Any],
|
| 10 |
+
Doc(
|
| 11 |
+
"""
|
| 12 |
+
Default configurations for Swagger UI.
|
| 13 |
+
|
| 14 |
+
You can use it as a template to add any other configurations needed.
|
| 15 |
+
"""
|
| 16 |
+
),
|
| 17 |
+
] = {
|
| 18 |
+
"dom_id": "#swagger-ui",
|
| 19 |
+
"layout": "BaseLayout",
|
| 20 |
+
"deepLinking": True,
|
| 21 |
+
"showExtensions": True,
|
| 22 |
+
"showCommonExtensions": True,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def get_swagger_ui_html(
|
| 27 |
+
*,
|
| 28 |
+
openapi_url: Annotated[
|
| 29 |
+
str,
|
| 30 |
+
Doc(
|
| 31 |
+
"""
|
| 32 |
+
The OpenAPI URL that Swagger UI should load and use.
|
| 33 |
+
|
| 34 |
+
This is normally done automatically by FastAPI using the default URL
|
| 35 |
+
`/openapi.json`.
|
| 36 |
+
"""
|
| 37 |
+
),
|
| 38 |
+
],
|
| 39 |
+
title: Annotated[
|
| 40 |
+
str,
|
| 41 |
+
Doc(
|
| 42 |
+
"""
|
| 43 |
+
The HTML `<title>` content, normally shown in the browser tab.
|
| 44 |
+
"""
|
| 45 |
+
),
|
| 46 |
+
],
|
| 47 |
+
swagger_js_url: Annotated[
|
| 48 |
+
str,
|
| 49 |
+
Doc(
|
| 50 |
+
"""
|
| 51 |
+
The URL to use to load the Swagger UI JavaScript.
|
| 52 |
+
|
| 53 |
+
It is normally set to a CDN URL.
|
| 54 |
+
"""
|
| 55 |
+
),
|
| 56 |
+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
|
| 57 |
+
swagger_css_url: Annotated[
|
| 58 |
+
str,
|
| 59 |
+
Doc(
|
| 60 |
+
"""
|
| 61 |
+
The URL to use to load the Swagger UI CSS.
|
| 62 |
+
|
| 63 |
+
It is normally set to a CDN URL.
|
| 64 |
+
"""
|
| 65 |
+
),
|
| 66 |
+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
|
| 67 |
+
swagger_favicon_url: Annotated[
|
| 68 |
+
str,
|
| 69 |
+
Doc(
|
| 70 |
+
"""
|
| 71 |
+
The URL of the favicon to use. It is normally shown in the browser tab.
|
| 72 |
+
"""
|
| 73 |
+
),
|
| 74 |
+
] = "https://fastapi.tiangolo.com/img/favicon.png",
|
| 75 |
+
oauth2_redirect_url: Annotated[
|
| 76 |
+
Optional[str],
|
| 77 |
+
Doc(
|
| 78 |
+
"""
|
| 79 |
+
The OAuth2 redirect URL, it is normally automatically handled by FastAPI.
|
| 80 |
+
"""
|
| 81 |
+
),
|
| 82 |
+
] = None,
|
| 83 |
+
init_oauth: Annotated[
|
| 84 |
+
Optional[Dict[str, Any]],
|
| 85 |
+
Doc(
|
| 86 |
+
"""
|
| 87 |
+
A dictionary with Swagger UI OAuth2 initialization configurations.
|
| 88 |
+
"""
|
| 89 |
+
),
|
| 90 |
+
] = None,
|
| 91 |
+
swagger_ui_parameters: Annotated[
|
| 92 |
+
Optional[Dict[str, Any]],
|
| 93 |
+
Doc(
|
| 94 |
+
"""
|
| 95 |
+
Configuration parameters for Swagger UI.
|
| 96 |
+
|
| 97 |
+
It defaults to [swagger_ui_default_parameters][fastapi.openapi.docs.swagger_ui_default_parameters].
|
| 98 |
+
"""
|
| 99 |
+
),
|
| 100 |
+
] = None,
|
| 101 |
+
) -> HTMLResponse:
|
| 102 |
+
"""
|
| 103 |
+
Generate and return the HTML that loads Swagger UI for the interactive
|
| 104 |
+
API docs (normally served at `/docs`).
|
| 105 |
+
|
| 106 |
+
You would only call this function yourself if you needed to override some parts,
|
| 107 |
+
for example the URLs to use to load Swagger UI's JavaScript and CSS.
|
| 108 |
+
|
| 109 |
+
Read more about it in the
|
| 110 |
+
[FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/)
|
| 111 |
+
and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).
|
| 112 |
+
"""
|
| 113 |
+
current_swagger_ui_parameters = swagger_ui_default_parameters.copy()
|
| 114 |
+
if swagger_ui_parameters:
|
| 115 |
+
current_swagger_ui_parameters.update(swagger_ui_parameters)
|
| 116 |
+
|
| 117 |
+
html = f"""
|
| 118 |
+
<!DOCTYPE html>
|
| 119 |
+
<html>
|
| 120 |
+
<head>
|
| 121 |
+
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
|
| 122 |
+
<link rel="shortcut icon" href="{swagger_favicon_url}">
|
| 123 |
+
<title>{title}</title>
|
| 124 |
+
</head>
|
| 125 |
+
<body>
|
| 126 |
+
<div id="swagger-ui">
|
| 127 |
+
</div>
|
| 128 |
+
<script src="{swagger_js_url}"></script>
|
| 129 |
+
<!-- `SwaggerUIBundle` is now available on the page -->
|
| 130 |
+
<script>
|
| 131 |
+
const ui = SwaggerUIBundle({{
|
| 132 |
+
url: '{openapi_url}',
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
for key, value in current_swagger_ui_parameters.items():
|
| 136 |
+
html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n"
|
| 137 |
+
|
| 138 |
+
if oauth2_redirect_url:
|
| 139 |
+
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
|
| 140 |
+
|
| 141 |
+
html += """
|
| 142 |
+
presets: [
|
| 143 |
+
SwaggerUIBundle.presets.apis,
|
| 144 |
+
SwaggerUIBundle.SwaggerUIStandalonePreset
|
| 145 |
+
],
|
| 146 |
+
})"""
|
| 147 |
+
|
| 148 |
+
if init_oauth:
|
| 149 |
+
html += f"""
|
| 150 |
+
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
|
| 151 |
+
"""
|
| 152 |
+
|
| 153 |
+
html += """
|
| 154 |
+
</script>
|
| 155 |
+
</body>
|
| 156 |
+
</html>
|
| 157 |
+
"""
|
| 158 |
+
return HTMLResponse(html)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def get_redoc_html(
|
| 162 |
+
*,
|
| 163 |
+
openapi_url: Annotated[
|
| 164 |
+
str,
|
| 165 |
+
Doc(
|
| 166 |
+
"""
|
| 167 |
+
The OpenAPI URL that ReDoc should load and use.
|
| 168 |
+
|
| 169 |
+
This is normally done automatically by FastAPI using the default URL
|
| 170 |
+
`/openapi.json`.
|
| 171 |
+
"""
|
| 172 |
+
),
|
| 173 |
+
],
|
| 174 |
+
title: Annotated[
|
| 175 |
+
str,
|
| 176 |
+
Doc(
|
| 177 |
+
"""
|
| 178 |
+
The HTML `<title>` content, normally shown in the browser tab.
|
| 179 |
+
"""
|
| 180 |
+
),
|
| 181 |
+
],
|
| 182 |
+
redoc_js_url: Annotated[
|
| 183 |
+
str,
|
| 184 |
+
Doc(
|
| 185 |
+
"""
|
| 186 |
+
The URL to use to load the ReDoc JavaScript.
|
| 187 |
+
|
| 188 |
+
It is normally set to a CDN URL.
|
| 189 |
+
"""
|
| 190 |
+
),
|
| 191 |
+
] = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
|
| 192 |
+
redoc_favicon_url: Annotated[
|
| 193 |
+
str,
|
| 194 |
+
Doc(
|
| 195 |
+
"""
|
| 196 |
+
The URL of the favicon to use. It is normally shown in the browser tab.
|
| 197 |
+
"""
|
| 198 |
+
),
|
| 199 |
+
] = "https://fastapi.tiangolo.com/img/favicon.png",
|
| 200 |
+
with_google_fonts: Annotated[
|
| 201 |
+
bool,
|
| 202 |
+
Doc(
|
| 203 |
+
"""
|
| 204 |
+
Load and use Google Fonts.
|
| 205 |
+
"""
|
| 206 |
+
),
|
| 207 |
+
] = True,
|
| 208 |
+
) -> HTMLResponse:
|
| 209 |
+
"""
|
| 210 |
+
Generate and return the HTML response that loads ReDoc for the alternative
|
| 211 |
+
API docs (normally served at `/redoc`).
|
| 212 |
+
|
| 213 |
+
You would only call this function yourself if you needed to override some parts,
|
| 214 |
+
for example the URLs to use to load ReDoc's JavaScript and CSS.
|
| 215 |
+
|
| 216 |
+
Read more about it in the
|
| 217 |
+
[FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).
|
| 218 |
+
"""
|
| 219 |
+
html = f"""
|
| 220 |
+
<!DOCTYPE html>
|
| 221 |
+
<html>
|
| 222 |
+
<head>
|
| 223 |
+
<title>{title}</title>
|
| 224 |
+
<!-- needed for adaptive design -->
|
| 225 |
+
<meta charset="utf-8"/>
|
| 226 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 227 |
+
"""
|
| 228 |
+
if with_google_fonts:
|
| 229 |
+
html += """
|
| 230 |
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
| 231 |
+
"""
|
| 232 |
+
html += f"""
|
| 233 |
+
<link rel="shortcut icon" href="{redoc_favicon_url}">
|
| 234 |
+
<!--
|
| 235 |
+
ReDoc doesn't change outer page styles
|
| 236 |
+
-->
|
| 237 |
+
<style>
|
| 238 |
+
body {{
|
| 239 |
+
margin: 0;
|
| 240 |
+
padding: 0;
|
| 241 |
+
}}
|
| 242 |
+
</style>
|
| 243 |
+
</head>
|
| 244 |
+
<body>
|
| 245 |
+
<noscript>
|
| 246 |
+
ReDoc requires Javascript to function. Please enable it to browse the documentation.
|
| 247 |
+
</noscript>
|
| 248 |
+
<redoc spec-url="{openapi_url}"></redoc>
|
| 249 |
+
<script src="{redoc_js_url}"> </script>
|
| 250 |
+
</body>
|
| 251 |
+
</html>
|
| 252 |
+
"""
|
| 253 |
+
return HTMLResponse(html)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
|
| 257 |
+
"""
|
| 258 |
+
Generate the HTML response with the OAuth2 redirection for Swagger UI.
|
| 259 |
+
|
| 260 |
+
You normally don't need to use or change this.
|
| 261 |
+
"""
|
| 262 |
+
# copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html
|
| 263 |
+
html = """
|
| 264 |
+
<!doctype html>
|
| 265 |
+
<html lang="en-US">
|
| 266 |
+
<head>
|
| 267 |
+
<title>Swagger UI: OAuth2 Redirect</title>
|
| 268 |
+
</head>
|
| 269 |
+
<body>
|
| 270 |
+
<script>
|
| 271 |
+
'use strict';
|
| 272 |
+
function run () {
|
| 273 |
+
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
| 274 |
+
var sentState = oauth2.state;
|
| 275 |
+
var redirectUrl = oauth2.redirectUrl;
|
| 276 |
+
var isValid, qp, arr;
|
| 277 |
+
|
| 278 |
+
if (/code|token|error/.test(window.location.hash)) {
|
| 279 |
+
qp = window.location.hash.substring(1).replace('?', '&');
|
| 280 |
+
} else {
|
| 281 |
+
qp = location.search.substring(1);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
arr = qp.split("&");
|
| 285 |
+
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
| 286 |
+
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
| 287 |
+
function (key, value) {
|
| 288 |
+
return key === "" ? value : decodeURIComponent(value);
|
| 289 |
+
}
|
| 290 |
+
) : {};
|
| 291 |
+
|
| 292 |
+
isValid = qp.state === sentState;
|
| 293 |
+
|
| 294 |
+
if ((
|
| 295 |
+
oauth2.auth.schema.get("flow") === "accessCode" ||
|
| 296 |
+
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
| 297 |
+
oauth2.auth.schema.get("flow") === "authorization_code"
|
| 298 |
+
) && !oauth2.auth.code) {
|
| 299 |
+
if (!isValid) {
|
| 300 |
+
oauth2.errCb({
|
| 301 |
+
authId: oauth2.auth.name,
|
| 302 |
+
source: "auth",
|
| 303 |
+
level: "warning",
|
| 304 |
+
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
| 305 |
+
});
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
if (qp.code) {
|
| 309 |
+
delete oauth2.state;
|
| 310 |
+
oauth2.auth.code = qp.code;
|
| 311 |
+
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
| 312 |
+
} else {
|
| 313 |
+
let oauthErrorMsg;
|
| 314 |
+
if (qp.error) {
|
| 315 |
+
oauthErrorMsg = "["+qp.error+"]: " +
|
| 316 |
+
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
| 317 |
+
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
oauth2.errCb({
|
| 321 |
+
authId: oauth2.auth.name,
|
| 322 |
+
source: "auth",
|
| 323 |
+
level: "error",
|
| 324 |
+
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
| 325 |
+
});
|
| 326 |
+
}
|
| 327 |
+
} else {
|
| 328 |
+
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
| 329 |
+
}
|
| 330 |
+
window.close();
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
if (document.readyState !== 'loading') {
|
| 334 |
+
run();
|
| 335 |
+
} else {
|
| 336 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 337 |
+
run();
|
| 338 |
+
});
|
| 339 |
+
}
|
| 340 |
+
</script>
|
| 341 |
+
</body>
|
| 342 |
+
</html>
|
| 343 |
+
"""
|
| 344 |
+
return HTMLResponse(content=html)
|
.venv/lib/python3.11/site-packages/fastapi/openapi/models.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union
|
| 3 |
+
|
| 4 |
+
from fastapi._compat import (
|
| 5 |
+
PYDANTIC_V2,
|
| 6 |
+
CoreSchema,
|
| 7 |
+
GetJsonSchemaHandler,
|
| 8 |
+
JsonSchemaValue,
|
| 9 |
+
_model_rebuild,
|
| 10 |
+
with_info_plain_validator_function,
|
| 11 |
+
)
|
| 12 |
+
from fastapi.logger import logger
|
| 13 |
+
from pydantic import AnyUrl, BaseModel, Field
|
| 14 |
+
from typing_extensions import Annotated, Literal, TypedDict
|
| 15 |
+
from typing_extensions import deprecated as typing_deprecated
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
import email_validator
|
| 19 |
+
|
| 20 |
+
assert email_validator # make autoflake ignore the unused import
|
| 21 |
+
from pydantic import EmailStr
|
| 22 |
+
except ImportError: # pragma: no cover
|
| 23 |
+
|
| 24 |
+
class EmailStr(str): # type: ignore
|
| 25 |
+
@classmethod
|
| 26 |
+
def __get_validators__(cls) -> Iterable[Callable[..., Any]]:
|
| 27 |
+
yield cls.validate
|
| 28 |
+
|
| 29 |
+
@classmethod
|
| 30 |
+
def validate(cls, v: Any) -> str:
|
| 31 |
+
logger.warning(
|
| 32 |
+
"email-validator not installed, email fields will be treated as str.\n"
|
| 33 |
+
"To install, run: pip install email-validator"
|
| 34 |
+
)
|
| 35 |
+
return str(v)
|
| 36 |
+
|
| 37 |
+
@classmethod
|
| 38 |
+
def _validate(cls, __input_value: Any, _: Any) -> str:
|
| 39 |
+
logger.warning(
|
| 40 |
+
"email-validator not installed, email fields will be treated as str.\n"
|
| 41 |
+
"To install, run: pip install email-validator"
|
| 42 |
+
)
|
| 43 |
+
return str(__input_value)
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def __get_pydantic_json_schema__(
|
| 47 |
+
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
| 48 |
+
) -> JsonSchemaValue:
|
| 49 |
+
return {"type": "string", "format": "email"}
|
| 50 |
+
|
| 51 |
+
@classmethod
|
| 52 |
+
def __get_pydantic_core_schema__(
|
| 53 |
+
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
| 54 |
+
) -> CoreSchema:
|
| 55 |
+
return with_info_plain_validator_function(cls._validate)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class BaseModelWithConfig(BaseModel):
|
| 59 |
+
if PYDANTIC_V2:
|
| 60 |
+
model_config = {"extra": "allow"}
|
| 61 |
+
|
| 62 |
+
else:
|
| 63 |
+
|
| 64 |
+
class Config:
|
| 65 |
+
extra = "allow"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class Contact(BaseModelWithConfig):
|
| 69 |
+
name: Optional[str] = None
|
| 70 |
+
url: Optional[AnyUrl] = None
|
| 71 |
+
email: Optional[EmailStr] = None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class License(BaseModelWithConfig):
|
| 75 |
+
name: str
|
| 76 |
+
identifier: Optional[str] = None
|
| 77 |
+
url: Optional[AnyUrl] = None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class Info(BaseModelWithConfig):
|
| 81 |
+
title: str
|
| 82 |
+
summary: Optional[str] = None
|
| 83 |
+
description: Optional[str] = None
|
| 84 |
+
termsOfService: Optional[str] = None
|
| 85 |
+
contact: Optional[Contact] = None
|
| 86 |
+
license: Optional[License] = None
|
| 87 |
+
version: str
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class ServerVariable(BaseModelWithConfig):
|
| 91 |
+
enum: Annotated[Optional[List[str]], Field(min_length=1)] = None
|
| 92 |
+
default: str
|
| 93 |
+
description: Optional[str] = None
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class Server(BaseModelWithConfig):
|
| 97 |
+
url: Union[AnyUrl, str]
|
| 98 |
+
description: Optional[str] = None
|
| 99 |
+
variables: Optional[Dict[str, ServerVariable]] = None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class Reference(BaseModel):
|
| 103 |
+
ref: str = Field(alias="$ref")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class Discriminator(BaseModel):
|
| 107 |
+
propertyName: str
|
| 108 |
+
mapping: Optional[Dict[str, str]] = None
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class XML(BaseModelWithConfig):
|
| 112 |
+
name: Optional[str] = None
|
| 113 |
+
namespace: Optional[str] = None
|
| 114 |
+
prefix: Optional[str] = None
|
| 115 |
+
attribute: Optional[bool] = None
|
| 116 |
+
wrapped: Optional[bool] = None
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class ExternalDocumentation(BaseModelWithConfig):
|
| 120 |
+
description: Optional[str] = None
|
| 121 |
+
url: AnyUrl
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class Schema(BaseModelWithConfig):
|
| 125 |
+
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu
|
| 126 |
+
# Core Vocabulary
|
| 127 |
+
schema_: Optional[str] = Field(default=None, alias="$schema")
|
| 128 |
+
vocabulary: Optional[str] = Field(default=None, alias="$vocabulary")
|
| 129 |
+
id: Optional[str] = Field(default=None, alias="$id")
|
| 130 |
+
anchor: Optional[str] = Field(default=None, alias="$anchor")
|
| 131 |
+
dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor")
|
| 132 |
+
ref: Optional[str] = Field(default=None, alias="$ref")
|
| 133 |
+
dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef")
|
| 134 |
+
defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs")
|
| 135 |
+
comment: Optional[str] = Field(default=None, alias="$comment")
|
| 136 |
+
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s
|
| 137 |
+
# A Vocabulary for Applying Subschemas
|
| 138 |
+
allOf: Optional[List["SchemaOrBool"]] = None
|
| 139 |
+
anyOf: Optional[List["SchemaOrBool"]] = None
|
| 140 |
+
oneOf: Optional[List["SchemaOrBool"]] = None
|
| 141 |
+
not_: Optional["SchemaOrBool"] = Field(default=None, alias="not")
|
| 142 |
+
if_: Optional["SchemaOrBool"] = Field(default=None, alias="if")
|
| 143 |
+
then: Optional["SchemaOrBool"] = None
|
| 144 |
+
else_: Optional["SchemaOrBool"] = Field(default=None, alias="else")
|
| 145 |
+
dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None
|
| 146 |
+
prefixItems: Optional[List["SchemaOrBool"]] = None
|
| 147 |
+
# TODO: uncomment and remove below when deprecating Pydantic v1
|
| 148 |
+
# It generales a list of schemas for tuples, before prefixItems was available
|
| 149 |
+
# items: Optional["SchemaOrBool"] = None
|
| 150 |
+
items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None
|
| 151 |
+
contains: Optional["SchemaOrBool"] = None
|
| 152 |
+
properties: Optional[Dict[str, "SchemaOrBool"]] = None
|
| 153 |
+
patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None
|
| 154 |
+
additionalProperties: Optional["SchemaOrBool"] = None
|
| 155 |
+
propertyNames: Optional["SchemaOrBool"] = None
|
| 156 |
+
unevaluatedItems: Optional["SchemaOrBool"] = None
|
| 157 |
+
unevaluatedProperties: Optional["SchemaOrBool"] = None
|
| 158 |
+
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural
|
| 159 |
+
# A Vocabulary for Structural Validation
|
| 160 |
+
type: Optional[str] = None
|
| 161 |
+
enum: Optional[List[Any]] = None
|
| 162 |
+
const: Optional[Any] = None
|
| 163 |
+
multipleOf: Optional[float] = Field(default=None, gt=0)
|
| 164 |
+
maximum: Optional[float] = None
|
| 165 |
+
exclusiveMaximum: Optional[float] = None
|
| 166 |
+
minimum: Optional[float] = None
|
| 167 |
+
exclusiveMinimum: Optional[float] = None
|
| 168 |
+
maxLength: Optional[int] = Field(default=None, ge=0)
|
| 169 |
+
minLength: Optional[int] = Field(default=None, ge=0)
|
| 170 |
+
pattern: Optional[str] = None
|
| 171 |
+
maxItems: Optional[int] = Field(default=None, ge=0)
|
| 172 |
+
minItems: Optional[int] = Field(default=None, ge=0)
|
| 173 |
+
uniqueItems: Optional[bool] = None
|
| 174 |
+
maxContains: Optional[int] = Field(default=None, ge=0)
|
| 175 |
+
minContains: Optional[int] = Field(default=None, ge=0)
|
| 176 |
+
maxProperties: Optional[int] = Field(default=None, ge=0)
|
| 177 |
+
minProperties: Optional[int] = Field(default=None, ge=0)
|
| 178 |
+
required: Optional[List[str]] = None
|
| 179 |
+
dependentRequired: Optional[Dict[str, Set[str]]] = None
|
| 180 |
+
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c
|
| 181 |
+
# Vocabularies for Semantic Content With "format"
|
| 182 |
+
format: Optional[str] = None
|
| 183 |
+
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten
|
| 184 |
+
# A Vocabulary for the Contents of String-Encoded Data
|
| 185 |
+
contentEncoding: Optional[str] = None
|
| 186 |
+
contentMediaType: Optional[str] = None
|
| 187 |
+
contentSchema: Optional["SchemaOrBool"] = None
|
| 188 |
+
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta
|
| 189 |
+
# A Vocabulary for Basic Meta-Data Annotations
|
| 190 |
+
title: Optional[str] = None
|
| 191 |
+
description: Optional[str] = None
|
| 192 |
+
default: Optional[Any] = None
|
| 193 |
+
deprecated: Optional[bool] = None
|
| 194 |
+
readOnly: Optional[bool] = None
|
| 195 |
+
writeOnly: Optional[bool] = None
|
| 196 |
+
examples: Optional[List[Any]] = None
|
| 197 |
+
# Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object
|
| 198 |
+
# Schema Object
|
| 199 |
+
discriminator: Optional[Discriminator] = None
|
| 200 |
+
xml: Optional[XML] = None
|
| 201 |
+
externalDocs: Optional[ExternalDocumentation] = None
|
| 202 |
+
example: Annotated[
|
| 203 |
+
Optional[Any],
|
| 204 |
+
typing_deprecated(
|
| 205 |
+
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
| 206 |
+
"although still supported. Use examples instead."
|
| 207 |
+
),
|
| 208 |
+
] = None
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
|
| 212 |
+
# A JSON Schema MUST be an object or a boolean.
|
| 213 |
+
SchemaOrBool = Union[Schema, bool]
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class Example(TypedDict, total=False):
|
| 217 |
+
summary: Optional[str]
|
| 218 |
+
description: Optional[str]
|
| 219 |
+
value: Optional[Any]
|
| 220 |
+
externalValue: Optional[AnyUrl]
|
| 221 |
+
|
| 222 |
+
if PYDANTIC_V2: # type: ignore [misc]
|
| 223 |
+
__pydantic_config__ = {"extra": "allow"}
|
| 224 |
+
|
| 225 |
+
else:
|
| 226 |
+
|
| 227 |
+
class Config:
|
| 228 |
+
extra = "allow"
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class ParameterInType(Enum):
|
| 232 |
+
query = "query"
|
| 233 |
+
header = "header"
|
| 234 |
+
path = "path"
|
| 235 |
+
cookie = "cookie"
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
class Encoding(BaseModelWithConfig):
|
| 239 |
+
contentType: Optional[str] = None
|
| 240 |
+
headers: Optional[Dict[str, Union["Header", Reference]]] = None
|
| 241 |
+
style: Optional[str] = None
|
| 242 |
+
explode: Optional[bool] = None
|
| 243 |
+
allowReserved: Optional[bool] = None
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
class MediaType(BaseModelWithConfig):
|
| 247 |
+
schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema")
|
| 248 |
+
example: Optional[Any] = None
|
| 249 |
+
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
| 250 |
+
encoding: Optional[Dict[str, Encoding]] = None
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
class ParameterBase(BaseModelWithConfig):
|
| 254 |
+
description: Optional[str] = None
|
| 255 |
+
required: Optional[bool] = None
|
| 256 |
+
deprecated: Optional[bool] = None
|
| 257 |
+
# Serialization rules for simple scenarios
|
| 258 |
+
style: Optional[str] = None
|
| 259 |
+
explode: Optional[bool] = None
|
| 260 |
+
allowReserved: Optional[bool] = None
|
| 261 |
+
schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema")
|
| 262 |
+
example: Optional[Any] = None
|
| 263 |
+
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
| 264 |
+
# Serialization rules for more complex scenarios
|
| 265 |
+
content: Optional[Dict[str, MediaType]] = None
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class Parameter(ParameterBase):
|
| 269 |
+
name: str
|
| 270 |
+
in_: ParameterInType = Field(alias="in")
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class Header(ParameterBase):
|
| 274 |
+
pass
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class RequestBody(BaseModelWithConfig):
|
| 278 |
+
description: Optional[str] = None
|
| 279 |
+
content: Dict[str, MediaType]
|
| 280 |
+
required: Optional[bool] = None
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
class Link(BaseModelWithConfig):
|
| 284 |
+
operationRef: Optional[str] = None
|
| 285 |
+
operationId: Optional[str] = None
|
| 286 |
+
parameters: Optional[Dict[str, Union[Any, str]]] = None
|
| 287 |
+
requestBody: Optional[Union[Any, str]] = None
|
| 288 |
+
description: Optional[str] = None
|
| 289 |
+
server: Optional[Server] = None
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
class Response(BaseModelWithConfig):
|
| 293 |
+
description: str
|
| 294 |
+
headers: Optional[Dict[str, Union[Header, Reference]]] = None
|
| 295 |
+
content: Optional[Dict[str, MediaType]] = None
|
| 296 |
+
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
class Operation(BaseModelWithConfig):
|
| 300 |
+
tags: Optional[List[str]] = None
|
| 301 |
+
summary: Optional[str] = None
|
| 302 |
+
description: Optional[str] = None
|
| 303 |
+
externalDocs: Optional[ExternalDocumentation] = None
|
| 304 |
+
operationId: Optional[str] = None
|
| 305 |
+
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
| 306 |
+
requestBody: Optional[Union[RequestBody, Reference]] = None
|
| 307 |
+
# Using Any for Specification Extensions
|
| 308 |
+
responses: Optional[Dict[str, Union[Response, Any]]] = None
|
| 309 |
+
callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None
|
| 310 |
+
deprecated: Optional[bool] = None
|
| 311 |
+
security: Optional[List[Dict[str, List[str]]]] = None
|
| 312 |
+
servers: Optional[List[Server]] = None
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
class PathItem(BaseModelWithConfig):
|
| 316 |
+
ref: Optional[str] = Field(default=None, alias="$ref")
|
| 317 |
+
summary: Optional[str] = None
|
| 318 |
+
description: Optional[str] = None
|
| 319 |
+
get: Optional[Operation] = None
|
| 320 |
+
put: Optional[Operation] = None
|
| 321 |
+
post: Optional[Operation] = None
|
| 322 |
+
delete: Optional[Operation] = None
|
| 323 |
+
options: Optional[Operation] = None
|
| 324 |
+
head: Optional[Operation] = None
|
| 325 |
+
patch: Optional[Operation] = None
|
| 326 |
+
trace: Optional[Operation] = None
|
| 327 |
+
servers: Optional[List[Server]] = None
|
| 328 |
+
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
class SecuritySchemeType(Enum):
|
| 332 |
+
apiKey = "apiKey"
|
| 333 |
+
http = "http"
|
| 334 |
+
oauth2 = "oauth2"
|
| 335 |
+
openIdConnect = "openIdConnect"
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
class SecurityBase(BaseModelWithConfig):
|
| 339 |
+
type_: SecuritySchemeType = Field(alias="type")
|
| 340 |
+
description: Optional[str] = None
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
class APIKeyIn(Enum):
|
| 344 |
+
query = "query"
|
| 345 |
+
header = "header"
|
| 346 |
+
cookie = "cookie"
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
class APIKey(SecurityBase):
|
| 350 |
+
type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type")
|
| 351 |
+
in_: APIKeyIn = Field(alias="in")
|
| 352 |
+
name: str
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
class HTTPBase(SecurityBase):
|
| 356 |
+
type_: SecuritySchemeType = Field(default=SecuritySchemeType.http, alias="type")
|
| 357 |
+
scheme: str
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
class HTTPBearer(HTTPBase):
|
| 361 |
+
scheme: Literal["bearer"] = "bearer"
|
| 362 |
+
bearerFormat: Optional[str] = None
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
class OAuthFlow(BaseModelWithConfig):
|
| 366 |
+
refreshUrl: Optional[str] = None
|
| 367 |
+
scopes: Dict[str, str] = {}
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
class OAuthFlowImplicit(OAuthFlow):
|
| 371 |
+
authorizationUrl: str
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
class OAuthFlowPassword(OAuthFlow):
|
| 375 |
+
tokenUrl: str
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
class OAuthFlowClientCredentials(OAuthFlow):
|
| 379 |
+
tokenUrl: str
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
class OAuthFlowAuthorizationCode(OAuthFlow):
|
| 383 |
+
authorizationUrl: str
|
| 384 |
+
tokenUrl: str
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
class OAuthFlows(BaseModelWithConfig):
|
| 388 |
+
implicit: Optional[OAuthFlowImplicit] = None
|
| 389 |
+
password: Optional[OAuthFlowPassword] = None
|
| 390 |
+
clientCredentials: Optional[OAuthFlowClientCredentials] = None
|
| 391 |
+
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
class OAuth2(SecurityBase):
|
| 395 |
+
type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type")
|
| 396 |
+
flows: OAuthFlows
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
class OpenIdConnect(SecurityBase):
|
| 400 |
+
type_: SecuritySchemeType = Field(
|
| 401 |
+
default=SecuritySchemeType.openIdConnect, alias="type"
|
| 402 |
+
)
|
| 403 |
+
openIdConnectUrl: str
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
class Components(BaseModelWithConfig):
|
| 410 |
+
schemas: Optional[Dict[str, Union[Schema, Reference]]] = None
|
| 411 |
+
responses: Optional[Dict[str, Union[Response, Reference]]] = None
|
| 412 |
+
parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None
|
| 413 |
+
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
| 414 |
+
requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None
|
| 415 |
+
headers: Optional[Dict[str, Union[Header, Reference]]] = None
|
| 416 |
+
securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None
|
| 417 |
+
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
| 418 |
+
# Using Any for Specification Extensions
|
| 419 |
+
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
|
| 420 |
+
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
class Tag(BaseModelWithConfig):
|
| 424 |
+
name: str
|
| 425 |
+
description: Optional[str] = None
|
| 426 |
+
externalDocs: Optional[ExternalDocumentation] = None
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
class OpenAPI(BaseModelWithConfig):
|
| 430 |
+
openapi: str
|
| 431 |
+
info: Info
|
| 432 |
+
jsonSchemaDialect: Optional[str] = None
|
| 433 |
+
servers: Optional[List[Server]] = None
|
| 434 |
+
# Using Any for Specification Extensions
|
| 435 |
+
paths: Optional[Dict[str, Union[PathItem, Any]]] = None
|
| 436 |
+
webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None
|
| 437 |
+
components: Optional[Components] = None
|
| 438 |
+
security: Optional[List[Dict[str, List[str]]]] = None
|
| 439 |
+
tags: Optional[List[Tag]] = None
|
| 440 |
+
externalDocs: Optional[ExternalDocumentation] = None
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
_model_rebuild(Schema)
|
| 444 |
+
_model_rebuild(Operation)
|
| 445 |
+
_model_rebuild(Encoding)
|
.venv/lib/python3.11/site-packages/fastapi/openapi/utils.py
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import http.client
|
| 2 |
+
import inspect
|
| 3 |
+
import warnings
|
| 4 |
+
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
| 5 |
+
|
| 6 |
+
from fastapi import routing
|
| 7 |
+
from fastapi._compat import (
|
| 8 |
+
GenerateJsonSchema,
|
| 9 |
+
JsonSchemaValue,
|
| 10 |
+
ModelField,
|
| 11 |
+
Undefined,
|
| 12 |
+
get_compat_model_name_map,
|
| 13 |
+
get_definitions,
|
| 14 |
+
get_schema_from_model_field,
|
| 15 |
+
lenient_issubclass,
|
| 16 |
+
)
|
| 17 |
+
from fastapi.datastructures import DefaultPlaceholder
|
| 18 |
+
from fastapi.dependencies.models import Dependant
|
| 19 |
+
from fastapi.dependencies.utils import (
|
| 20 |
+
_get_flat_fields_from_params,
|
| 21 |
+
get_flat_dependant,
|
| 22 |
+
get_flat_params,
|
| 23 |
+
)
|
| 24 |
+
from fastapi.encoders import jsonable_encoder
|
| 25 |
+
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
|
| 26 |
+
from fastapi.openapi.models import OpenAPI
|
| 27 |
+
from fastapi.params import Body, ParamTypes
|
| 28 |
+
from fastapi.responses import Response
|
| 29 |
+
from fastapi.types import ModelNameMap
|
| 30 |
+
from fastapi.utils import (
|
| 31 |
+
deep_dict_update,
|
| 32 |
+
generate_operation_id_for_path,
|
| 33 |
+
is_body_allowed_for_status_code,
|
| 34 |
+
)
|
| 35 |
+
from starlette.responses import JSONResponse
|
| 36 |
+
from starlette.routing import BaseRoute
|
| 37 |
+
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
| 38 |
+
from typing_extensions import Literal
|
| 39 |
+
|
| 40 |
+
validation_error_definition = {
|
| 41 |
+
"title": "ValidationError",
|
| 42 |
+
"type": "object",
|
| 43 |
+
"properties": {
|
| 44 |
+
"loc": {
|
| 45 |
+
"title": "Location",
|
| 46 |
+
"type": "array",
|
| 47 |
+
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
| 48 |
+
},
|
| 49 |
+
"msg": {"title": "Message", "type": "string"},
|
| 50 |
+
"type": {"title": "Error Type", "type": "string"},
|
| 51 |
+
},
|
| 52 |
+
"required": ["loc", "msg", "type"],
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
validation_error_response_definition = {
|
| 56 |
+
"title": "HTTPValidationError",
|
| 57 |
+
"type": "object",
|
| 58 |
+
"properties": {
|
| 59 |
+
"detail": {
|
| 60 |
+
"title": "Detail",
|
| 61 |
+
"type": "array",
|
| 62 |
+
"items": {"$ref": REF_PREFIX + "ValidationError"},
|
| 63 |
+
}
|
| 64 |
+
},
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
status_code_ranges: Dict[str, str] = {
|
| 68 |
+
"1XX": "Information",
|
| 69 |
+
"2XX": "Success",
|
| 70 |
+
"3XX": "Redirection",
|
| 71 |
+
"4XX": "Client Error",
|
| 72 |
+
"5XX": "Server Error",
|
| 73 |
+
"DEFAULT": "Default Response",
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_openapi_security_definitions(
|
| 78 |
+
flat_dependant: Dependant,
|
| 79 |
+
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
| 80 |
+
security_definitions = {}
|
| 81 |
+
operation_security = []
|
| 82 |
+
for security_requirement in flat_dependant.security_requirements:
|
| 83 |
+
security_definition = jsonable_encoder(
|
| 84 |
+
security_requirement.security_scheme.model,
|
| 85 |
+
by_alias=True,
|
| 86 |
+
exclude_none=True,
|
| 87 |
+
)
|
| 88 |
+
security_name = security_requirement.security_scheme.scheme_name
|
| 89 |
+
security_definitions[security_name] = security_definition
|
| 90 |
+
operation_security.append({security_name: security_requirement.scopes})
|
| 91 |
+
return security_definitions, operation_security
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _get_openapi_operation_parameters(
|
| 95 |
+
*,
|
| 96 |
+
dependant: Dependant,
|
| 97 |
+
schema_generator: GenerateJsonSchema,
|
| 98 |
+
model_name_map: ModelNameMap,
|
| 99 |
+
field_mapping: Dict[
|
| 100 |
+
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
| 101 |
+
],
|
| 102 |
+
separate_input_output_schemas: bool = True,
|
| 103 |
+
) -> List[Dict[str, Any]]:
|
| 104 |
+
parameters = []
|
| 105 |
+
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
| 106 |
+
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
|
| 107 |
+
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
|
| 108 |
+
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
|
| 109 |
+
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
|
| 110 |
+
parameter_groups = [
|
| 111 |
+
(ParamTypes.path, path_params),
|
| 112 |
+
(ParamTypes.query, query_params),
|
| 113 |
+
(ParamTypes.header, header_params),
|
| 114 |
+
(ParamTypes.cookie, cookie_params),
|
| 115 |
+
]
|
| 116 |
+
for param_type, param_group in parameter_groups:
|
| 117 |
+
for param in param_group:
|
| 118 |
+
field_info = param.field_info
|
| 119 |
+
# field_info = cast(Param, field_info)
|
| 120 |
+
if not getattr(field_info, "include_in_schema", True):
|
| 121 |
+
continue
|
| 122 |
+
param_schema = get_schema_from_model_field(
|
| 123 |
+
field=param,
|
| 124 |
+
schema_generator=schema_generator,
|
| 125 |
+
model_name_map=model_name_map,
|
| 126 |
+
field_mapping=field_mapping,
|
| 127 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 128 |
+
)
|
| 129 |
+
parameter = {
|
| 130 |
+
"name": param.alias,
|
| 131 |
+
"in": param_type.value,
|
| 132 |
+
"required": param.required,
|
| 133 |
+
"schema": param_schema,
|
| 134 |
+
}
|
| 135 |
+
if field_info.description:
|
| 136 |
+
parameter["description"] = field_info.description
|
| 137 |
+
openapi_examples = getattr(field_info, "openapi_examples", None)
|
| 138 |
+
example = getattr(field_info, "example", None)
|
| 139 |
+
if openapi_examples:
|
| 140 |
+
parameter["examples"] = jsonable_encoder(openapi_examples)
|
| 141 |
+
elif example != Undefined:
|
| 142 |
+
parameter["example"] = jsonable_encoder(example)
|
| 143 |
+
if getattr(field_info, "deprecated", None):
|
| 144 |
+
parameter["deprecated"] = True
|
| 145 |
+
parameters.append(parameter)
|
| 146 |
+
return parameters
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def get_openapi_operation_request_body(
|
| 150 |
+
*,
|
| 151 |
+
body_field: Optional[ModelField],
|
| 152 |
+
schema_generator: GenerateJsonSchema,
|
| 153 |
+
model_name_map: ModelNameMap,
|
| 154 |
+
field_mapping: Dict[
|
| 155 |
+
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
| 156 |
+
],
|
| 157 |
+
separate_input_output_schemas: bool = True,
|
| 158 |
+
) -> Optional[Dict[str, Any]]:
|
| 159 |
+
if not body_field:
|
| 160 |
+
return None
|
| 161 |
+
assert isinstance(body_field, ModelField)
|
| 162 |
+
body_schema = get_schema_from_model_field(
|
| 163 |
+
field=body_field,
|
| 164 |
+
schema_generator=schema_generator,
|
| 165 |
+
model_name_map=model_name_map,
|
| 166 |
+
field_mapping=field_mapping,
|
| 167 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 168 |
+
)
|
| 169 |
+
field_info = cast(Body, body_field.field_info)
|
| 170 |
+
request_media_type = field_info.media_type
|
| 171 |
+
required = body_field.required
|
| 172 |
+
request_body_oai: Dict[str, Any] = {}
|
| 173 |
+
if required:
|
| 174 |
+
request_body_oai["required"] = required
|
| 175 |
+
request_media_content: Dict[str, Any] = {"schema": body_schema}
|
| 176 |
+
if field_info.openapi_examples:
|
| 177 |
+
request_media_content["examples"] = jsonable_encoder(
|
| 178 |
+
field_info.openapi_examples
|
| 179 |
+
)
|
| 180 |
+
elif field_info.example != Undefined:
|
| 181 |
+
request_media_content["example"] = jsonable_encoder(field_info.example)
|
| 182 |
+
request_body_oai["content"] = {request_media_type: request_media_content}
|
| 183 |
+
return request_body_oai
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def generate_operation_id(
|
| 187 |
+
*, route: routing.APIRoute, method: str
|
| 188 |
+
) -> str: # pragma: nocover
|
| 189 |
+
warnings.warn(
|
| 190 |
+
"fastapi.openapi.utils.generate_operation_id() was deprecated, "
|
| 191 |
+
"it is not used internally, and will be removed soon",
|
| 192 |
+
DeprecationWarning,
|
| 193 |
+
stacklevel=2,
|
| 194 |
+
)
|
| 195 |
+
if route.operation_id:
|
| 196 |
+
return route.operation_id
|
| 197 |
+
path: str = route.path_format
|
| 198 |
+
return generate_operation_id_for_path(name=route.name, path=path, method=method)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
| 202 |
+
if route.summary:
|
| 203 |
+
return route.summary
|
| 204 |
+
return route.name.replace("_", " ").title()
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def get_openapi_operation_metadata(
|
| 208 |
+
*, route: routing.APIRoute, method: str, operation_ids: Set[str]
|
| 209 |
+
) -> Dict[str, Any]:
|
| 210 |
+
operation: Dict[str, Any] = {}
|
| 211 |
+
if route.tags:
|
| 212 |
+
operation["tags"] = route.tags
|
| 213 |
+
operation["summary"] = generate_operation_summary(route=route, method=method)
|
| 214 |
+
if route.description:
|
| 215 |
+
operation["description"] = route.description
|
| 216 |
+
operation_id = route.operation_id or route.unique_id
|
| 217 |
+
if operation_id in operation_ids:
|
| 218 |
+
message = (
|
| 219 |
+
f"Duplicate Operation ID {operation_id} for function "
|
| 220 |
+
+ f"{route.endpoint.__name__}"
|
| 221 |
+
)
|
| 222 |
+
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
|
| 223 |
+
if file_name:
|
| 224 |
+
message += f" at {file_name}"
|
| 225 |
+
warnings.warn(message, stacklevel=1)
|
| 226 |
+
operation_ids.add(operation_id)
|
| 227 |
+
operation["operationId"] = operation_id
|
| 228 |
+
if route.deprecated:
|
| 229 |
+
operation["deprecated"] = route.deprecated
|
| 230 |
+
return operation
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def get_openapi_path(
|
| 234 |
+
*,
|
| 235 |
+
route: routing.APIRoute,
|
| 236 |
+
operation_ids: Set[str],
|
| 237 |
+
schema_generator: GenerateJsonSchema,
|
| 238 |
+
model_name_map: ModelNameMap,
|
| 239 |
+
field_mapping: Dict[
|
| 240 |
+
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
| 241 |
+
],
|
| 242 |
+
separate_input_output_schemas: bool = True,
|
| 243 |
+
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
| 244 |
+
path = {}
|
| 245 |
+
security_schemes: Dict[str, Any] = {}
|
| 246 |
+
definitions: Dict[str, Any] = {}
|
| 247 |
+
assert route.methods is not None, "Methods must be a list"
|
| 248 |
+
if isinstance(route.response_class, DefaultPlaceholder):
|
| 249 |
+
current_response_class: Type[Response] = route.response_class.value
|
| 250 |
+
else:
|
| 251 |
+
current_response_class = route.response_class
|
| 252 |
+
assert current_response_class, "A response class is needed to generate OpenAPI"
|
| 253 |
+
route_response_media_type: Optional[str] = current_response_class.media_type
|
| 254 |
+
if route.include_in_schema:
|
| 255 |
+
for method in route.methods:
|
| 256 |
+
operation = get_openapi_operation_metadata(
|
| 257 |
+
route=route, method=method, operation_ids=operation_ids
|
| 258 |
+
)
|
| 259 |
+
parameters: List[Dict[str, Any]] = []
|
| 260 |
+
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
| 261 |
+
security_definitions, operation_security = get_openapi_security_definitions(
|
| 262 |
+
flat_dependant=flat_dependant
|
| 263 |
+
)
|
| 264 |
+
if operation_security:
|
| 265 |
+
operation.setdefault("security", []).extend(operation_security)
|
| 266 |
+
if security_definitions:
|
| 267 |
+
security_schemes.update(security_definitions)
|
| 268 |
+
operation_parameters = _get_openapi_operation_parameters(
|
| 269 |
+
dependant=route.dependant,
|
| 270 |
+
schema_generator=schema_generator,
|
| 271 |
+
model_name_map=model_name_map,
|
| 272 |
+
field_mapping=field_mapping,
|
| 273 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 274 |
+
)
|
| 275 |
+
parameters.extend(operation_parameters)
|
| 276 |
+
if parameters:
|
| 277 |
+
all_parameters = {
|
| 278 |
+
(param["in"], param["name"]): param for param in parameters
|
| 279 |
+
}
|
| 280 |
+
required_parameters = {
|
| 281 |
+
(param["in"], param["name"]): param
|
| 282 |
+
for param in parameters
|
| 283 |
+
if param.get("required")
|
| 284 |
+
}
|
| 285 |
+
# Make sure required definitions of the same parameter take precedence
|
| 286 |
+
# over non-required definitions
|
| 287 |
+
all_parameters.update(required_parameters)
|
| 288 |
+
operation["parameters"] = list(all_parameters.values())
|
| 289 |
+
if method in METHODS_WITH_BODY:
|
| 290 |
+
request_body_oai = get_openapi_operation_request_body(
|
| 291 |
+
body_field=route.body_field,
|
| 292 |
+
schema_generator=schema_generator,
|
| 293 |
+
model_name_map=model_name_map,
|
| 294 |
+
field_mapping=field_mapping,
|
| 295 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 296 |
+
)
|
| 297 |
+
if request_body_oai:
|
| 298 |
+
operation["requestBody"] = request_body_oai
|
| 299 |
+
if route.callbacks:
|
| 300 |
+
callbacks = {}
|
| 301 |
+
for callback in route.callbacks:
|
| 302 |
+
if isinstance(callback, routing.APIRoute):
|
| 303 |
+
(
|
| 304 |
+
cb_path,
|
| 305 |
+
cb_security_schemes,
|
| 306 |
+
cb_definitions,
|
| 307 |
+
) = get_openapi_path(
|
| 308 |
+
route=callback,
|
| 309 |
+
operation_ids=operation_ids,
|
| 310 |
+
schema_generator=schema_generator,
|
| 311 |
+
model_name_map=model_name_map,
|
| 312 |
+
field_mapping=field_mapping,
|
| 313 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 314 |
+
)
|
| 315 |
+
callbacks[callback.name] = {callback.path: cb_path}
|
| 316 |
+
operation["callbacks"] = callbacks
|
| 317 |
+
if route.status_code is not None:
|
| 318 |
+
status_code = str(route.status_code)
|
| 319 |
+
else:
|
| 320 |
+
# It would probably make more sense for all response classes to have an
|
| 321 |
+
# explicit default status_code, and to extract it from them, instead of
|
| 322 |
+
# doing this inspection tricks, that would probably be in the future
|
| 323 |
+
# TODO: probably make status_code a default class attribute for all
|
| 324 |
+
# responses in Starlette
|
| 325 |
+
response_signature = inspect.signature(current_response_class.__init__)
|
| 326 |
+
status_code_param = response_signature.parameters.get("status_code")
|
| 327 |
+
if status_code_param is not None:
|
| 328 |
+
if isinstance(status_code_param.default, int):
|
| 329 |
+
status_code = str(status_code_param.default)
|
| 330 |
+
operation.setdefault("responses", {}).setdefault(status_code, {})[
|
| 331 |
+
"description"
|
| 332 |
+
] = route.response_description
|
| 333 |
+
if route_response_media_type and is_body_allowed_for_status_code(
|
| 334 |
+
route.status_code
|
| 335 |
+
):
|
| 336 |
+
response_schema = {"type": "string"}
|
| 337 |
+
if lenient_issubclass(current_response_class, JSONResponse):
|
| 338 |
+
if route.response_field:
|
| 339 |
+
response_schema = get_schema_from_model_field(
|
| 340 |
+
field=route.response_field,
|
| 341 |
+
schema_generator=schema_generator,
|
| 342 |
+
model_name_map=model_name_map,
|
| 343 |
+
field_mapping=field_mapping,
|
| 344 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 345 |
+
)
|
| 346 |
+
else:
|
| 347 |
+
response_schema = {}
|
| 348 |
+
operation.setdefault("responses", {}).setdefault(
|
| 349 |
+
status_code, {}
|
| 350 |
+
).setdefault("content", {}).setdefault(route_response_media_type, {})[
|
| 351 |
+
"schema"
|
| 352 |
+
] = response_schema
|
| 353 |
+
if route.responses:
|
| 354 |
+
operation_responses = operation.setdefault("responses", {})
|
| 355 |
+
for (
|
| 356 |
+
additional_status_code,
|
| 357 |
+
additional_response,
|
| 358 |
+
) in route.responses.items():
|
| 359 |
+
process_response = additional_response.copy()
|
| 360 |
+
process_response.pop("model", None)
|
| 361 |
+
status_code_key = str(additional_status_code).upper()
|
| 362 |
+
if status_code_key == "DEFAULT":
|
| 363 |
+
status_code_key = "default"
|
| 364 |
+
openapi_response = operation_responses.setdefault(
|
| 365 |
+
status_code_key, {}
|
| 366 |
+
)
|
| 367 |
+
assert isinstance(
|
| 368 |
+
process_response, dict
|
| 369 |
+
), "An additional response must be a dict"
|
| 370 |
+
field = route.response_fields.get(additional_status_code)
|
| 371 |
+
additional_field_schema: Optional[Dict[str, Any]] = None
|
| 372 |
+
if field:
|
| 373 |
+
additional_field_schema = get_schema_from_model_field(
|
| 374 |
+
field=field,
|
| 375 |
+
schema_generator=schema_generator,
|
| 376 |
+
model_name_map=model_name_map,
|
| 377 |
+
field_mapping=field_mapping,
|
| 378 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 379 |
+
)
|
| 380 |
+
media_type = route_response_media_type or "application/json"
|
| 381 |
+
additional_schema = (
|
| 382 |
+
process_response.setdefault("content", {})
|
| 383 |
+
.setdefault(media_type, {})
|
| 384 |
+
.setdefault("schema", {})
|
| 385 |
+
)
|
| 386 |
+
deep_dict_update(additional_schema, additional_field_schema)
|
| 387 |
+
status_text: Optional[str] = status_code_ranges.get(
|
| 388 |
+
str(additional_status_code).upper()
|
| 389 |
+
) or http.client.responses.get(int(additional_status_code))
|
| 390 |
+
description = (
|
| 391 |
+
process_response.get("description")
|
| 392 |
+
or openapi_response.get("description")
|
| 393 |
+
or status_text
|
| 394 |
+
or "Additional Response"
|
| 395 |
+
)
|
| 396 |
+
deep_dict_update(openapi_response, process_response)
|
| 397 |
+
openapi_response["description"] = description
|
| 398 |
+
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
|
| 399 |
+
all_route_params = get_flat_params(route.dependant)
|
| 400 |
+
if (all_route_params or route.body_field) and not any(
|
| 401 |
+
status in operation["responses"]
|
| 402 |
+
for status in [http422, "4XX", "default"]
|
| 403 |
+
):
|
| 404 |
+
operation["responses"][http422] = {
|
| 405 |
+
"description": "Validation Error",
|
| 406 |
+
"content": {
|
| 407 |
+
"application/json": {
|
| 408 |
+
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
| 409 |
+
}
|
| 410 |
+
},
|
| 411 |
+
}
|
| 412 |
+
if "ValidationError" not in definitions:
|
| 413 |
+
definitions.update(
|
| 414 |
+
{
|
| 415 |
+
"ValidationError": validation_error_definition,
|
| 416 |
+
"HTTPValidationError": validation_error_response_definition,
|
| 417 |
+
}
|
| 418 |
+
)
|
| 419 |
+
if route.openapi_extra:
|
| 420 |
+
deep_dict_update(operation, route.openapi_extra)
|
| 421 |
+
path[method.lower()] = operation
|
| 422 |
+
return path, security_schemes, definitions
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def get_fields_from_routes(
|
| 426 |
+
routes: Sequence[BaseRoute],
|
| 427 |
+
) -> List[ModelField]:
|
| 428 |
+
body_fields_from_routes: List[ModelField] = []
|
| 429 |
+
responses_from_routes: List[ModelField] = []
|
| 430 |
+
request_fields_from_routes: List[ModelField] = []
|
| 431 |
+
callback_flat_models: List[ModelField] = []
|
| 432 |
+
for route in routes:
|
| 433 |
+
if getattr(route, "include_in_schema", None) and isinstance(
|
| 434 |
+
route, routing.APIRoute
|
| 435 |
+
):
|
| 436 |
+
if route.body_field:
|
| 437 |
+
assert isinstance(
|
| 438 |
+
route.body_field, ModelField
|
| 439 |
+
), "A request body must be a Pydantic Field"
|
| 440 |
+
body_fields_from_routes.append(route.body_field)
|
| 441 |
+
if route.response_field:
|
| 442 |
+
responses_from_routes.append(route.response_field)
|
| 443 |
+
if route.response_fields:
|
| 444 |
+
responses_from_routes.extend(route.response_fields.values())
|
| 445 |
+
if route.callbacks:
|
| 446 |
+
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
|
| 447 |
+
params = get_flat_params(route.dependant)
|
| 448 |
+
request_fields_from_routes.extend(params)
|
| 449 |
+
|
| 450 |
+
flat_models = callback_flat_models + list(
|
| 451 |
+
body_fields_from_routes + responses_from_routes + request_fields_from_routes
|
| 452 |
+
)
|
| 453 |
+
return flat_models
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def get_openapi(
|
| 457 |
+
*,
|
| 458 |
+
title: str,
|
| 459 |
+
version: str,
|
| 460 |
+
openapi_version: str = "3.1.0",
|
| 461 |
+
summary: Optional[str] = None,
|
| 462 |
+
description: Optional[str] = None,
|
| 463 |
+
routes: Sequence[BaseRoute],
|
| 464 |
+
webhooks: Optional[Sequence[BaseRoute]] = None,
|
| 465 |
+
tags: Optional[List[Dict[str, Any]]] = None,
|
| 466 |
+
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
|
| 467 |
+
terms_of_service: Optional[str] = None,
|
| 468 |
+
contact: Optional[Dict[str, Union[str, Any]]] = None,
|
| 469 |
+
license_info: Optional[Dict[str, Union[str, Any]]] = None,
|
| 470 |
+
separate_input_output_schemas: bool = True,
|
| 471 |
+
) -> Dict[str, Any]:
|
| 472 |
+
info: Dict[str, Any] = {"title": title, "version": version}
|
| 473 |
+
if summary:
|
| 474 |
+
info["summary"] = summary
|
| 475 |
+
if description:
|
| 476 |
+
info["description"] = description
|
| 477 |
+
if terms_of_service:
|
| 478 |
+
info["termsOfService"] = terms_of_service
|
| 479 |
+
if contact:
|
| 480 |
+
info["contact"] = contact
|
| 481 |
+
if license_info:
|
| 482 |
+
info["license"] = license_info
|
| 483 |
+
output: Dict[str, Any] = {"openapi": openapi_version, "info": info}
|
| 484 |
+
if servers:
|
| 485 |
+
output["servers"] = servers
|
| 486 |
+
components: Dict[str, Dict[str, Any]] = {}
|
| 487 |
+
paths: Dict[str, Dict[str, Any]] = {}
|
| 488 |
+
webhook_paths: Dict[str, Dict[str, Any]] = {}
|
| 489 |
+
operation_ids: Set[str] = set()
|
| 490 |
+
all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
|
| 491 |
+
model_name_map = get_compat_model_name_map(all_fields)
|
| 492 |
+
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
| 493 |
+
field_mapping, definitions = get_definitions(
|
| 494 |
+
fields=all_fields,
|
| 495 |
+
schema_generator=schema_generator,
|
| 496 |
+
model_name_map=model_name_map,
|
| 497 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 498 |
+
)
|
| 499 |
+
for route in routes or []:
|
| 500 |
+
if isinstance(route, routing.APIRoute):
|
| 501 |
+
result = get_openapi_path(
|
| 502 |
+
route=route,
|
| 503 |
+
operation_ids=operation_ids,
|
| 504 |
+
schema_generator=schema_generator,
|
| 505 |
+
model_name_map=model_name_map,
|
| 506 |
+
field_mapping=field_mapping,
|
| 507 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 508 |
+
)
|
| 509 |
+
if result:
|
| 510 |
+
path, security_schemes, path_definitions = result
|
| 511 |
+
if path:
|
| 512 |
+
paths.setdefault(route.path_format, {}).update(path)
|
| 513 |
+
if security_schemes:
|
| 514 |
+
components.setdefault("securitySchemes", {}).update(
|
| 515 |
+
security_schemes
|
| 516 |
+
)
|
| 517 |
+
if path_definitions:
|
| 518 |
+
definitions.update(path_definitions)
|
| 519 |
+
for webhook in webhooks or []:
|
| 520 |
+
if isinstance(webhook, routing.APIRoute):
|
| 521 |
+
result = get_openapi_path(
|
| 522 |
+
route=webhook,
|
| 523 |
+
operation_ids=operation_ids,
|
| 524 |
+
schema_generator=schema_generator,
|
| 525 |
+
model_name_map=model_name_map,
|
| 526 |
+
field_mapping=field_mapping,
|
| 527 |
+
separate_input_output_schemas=separate_input_output_schemas,
|
| 528 |
+
)
|
| 529 |
+
if result:
|
| 530 |
+
path, security_schemes, path_definitions = result
|
| 531 |
+
if path:
|
| 532 |
+
webhook_paths.setdefault(webhook.path_format, {}).update(path)
|
| 533 |
+
if security_schemes:
|
| 534 |
+
components.setdefault("securitySchemes", {}).update(
|
| 535 |
+
security_schemes
|
| 536 |
+
)
|
| 537 |
+
if path_definitions:
|
| 538 |
+
definitions.update(path_definitions)
|
| 539 |
+
if definitions:
|
| 540 |
+
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
| 541 |
+
if components:
|
| 542 |
+
output["components"] = components
|
| 543 |
+
output["paths"] = paths
|
| 544 |
+
if webhook_paths:
|
| 545 |
+
output["webhooks"] = webhook_paths
|
| 546 |
+
if tags:
|
| 547 |
+
output["tags"] = tags
|
| 548 |
+
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore
|
.venv/lib/python3.11/site-packages/fastapi/security/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .api_key import APIKeyCookie as APIKeyCookie
|
| 2 |
+
from .api_key import APIKeyHeader as APIKeyHeader
|
| 3 |
+
from .api_key import APIKeyQuery as APIKeyQuery
|
| 4 |
+
from .http import HTTPAuthorizationCredentials as HTTPAuthorizationCredentials
|
| 5 |
+
from .http import HTTPBasic as HTTPBasic
|
| 6 |
+
from .http import HTTPBasicCredentials as HTTPBasicCredentials
|
| 7 |
+
from .http import HTTPBearer as HTTPBearer
|
| 8 |
+
from .http import HTTPDigest as HTTPDigest
|
| 9 |
+
from .oauth2 import OAuth2 as OAuth2
|
| 10 |
+
from .oauth2 import OAuth2AuthorizationCodeBearer as OAuth2AuthorizationCodeBearer
|
| 11 |
+
from .oauth2 import OAuth2PasswordBearer as OAuth2PasswordBearer
|
| 12 |
+
from .oauth2 import OAuth2PasswordRequestForm as OAuth2PasswordRequestForm
|
| 13 |
+
from .oauth2 import OAuth2PasswordRequestFormStrict as OAuth2PasswordRequestFormStrict
|
| 14 |
+
from .oauth2 import SecurityScopes as SecurityScopes
|
| 15 |
+
from .open_id_connect_url import OpenIdConnect as OpenIdConnect
|
.venv/lib/python3.11/site-packages/fastapi/security/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (1.07 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/security/__pycache__/api_key.cpython-311.pyc
ADDED
|
Binary file (10.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/security/__pycache__/base.cpython-311.pyc
ADDED
|
Binary file (588 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/security/__pycache__/http.cpython-311.pyc
ADDED
|
Binary file (15.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/security/__pycache__/oauth2.cpython-311.pyc
ADDED
|
Binary file (20.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/security/__pycache__/open_id_connect_url.cpython-311.pyc
ADDED
|
Binary file (3.54 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/security/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (668 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/fastapi/security/api_key.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
from fastapi.openapi.models import APIKey, APIKeyIn
|
| 4 |
+
from fastapi.security.base import SecurityBase
|
| 5 |
+
from starlette.exceptions import HTTPException
|
| 6 |
+
from starlette.requests import Request
|
| 7 |
+
from starlette.status import HTTP_403_FORBIDDEN
|
| 8 |
+
from typing_extensions import Annotated, Doc
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class APIKeyBase(SecurityBase):
|
| 12 |
+
@staticmethod
|
| 13 |
+
def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
|
| 14 |
+
if not api_key:
|
| 15 |
+
if auto_error:
|
| 16 |
+
raise HTTPException(
|
| 17 |
+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
| 18 |
+
)
|
| 19 |
+
return None
|
| 20 |
+
return api_key
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class APIKeyQuery(APIKeyBase):
|
| 24 |
+
"""
|
| 25 |
+
API key authentication using a query parameter.
|
| 26 |
+
|
| 27 |
+
This defines the name of the query parameter that should be provided in the request
|
| 28 |
+
with the API key and integrates that into the OpenAPI documentation. It extracts
|
| 29 |
+
the key value sent in the query parameter automatically and provides it as the
|
| 30 |
+
dependency result. But it doesn't define how to send that API key to the client.
|
| 31 |
+
|
| 32 |
+
## Usage
|
| 33 |
+
|
| 34 |
+
Create an instance object and use that object as the dependency in `Depends()`.
|
| 35 |
+
|
| 36 |
+
The dependency result will be a string containing the key value.
|
| 37 |
+
|
| 38 |
+
## Example
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
from fastapi import Depends, FastAPI
|
| 42 |
+
from fastapi.security import APIKeyQuery
|
| 43 |
+
|
| 44 |
+
app = FastAPI()
|
| 45 |
+
|
| 46 |
+
query_scheme = APIKeyQuery(name="api_key")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@app.get("/items/")
|
| 50 |
+
async def read_items(api_key: str = Depends(query_scheme)):
|
| 51 |
+
return {"api_key": api_key}
|
| 52 |
+
```
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def __init__(
|
| 56 |
+
self,
|
| 57 |
+
*,
|
| 58 |
+
name: Annotated[
|
| 59 |
+
str,
|
| 60 |
+
Doc("Query parameter name."),
|
| 61 |
+
],
|
| 62 |
+
scheme_name: Annotated[
|
| 63 |
+
Optional[str],
|
| 64 |
+
Doc(
|
| 65 |
+
"""
|
| 66 |
+
Security scheme name.
|
| 67 |
+
|
| 68 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 69 |
+
"""
|
| 70 |
+
),
|
| 71 |
+
] = None,
|
| 72 |
+
description: Annotated[
|
| 73 |
+
Optional[str],
|
| 74 |
+
Doc(
|
| 75 |
+
"""
|
| 76 |
+
Security scheme description.
|
| 77 |
+
|
| 78 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 79 |
+
"""
|
| 80 |
+
),
|
| 81 |
+
] = None,
|
| 82 |
+
auto_error: Annotated[
|
| 83 |
+
bool,
|
| 84 |
+
Doc(
|
| 85 |
+
"""
|
| 86 |
+
By default, if the query parameter is not provided, `APIKeyQuery` will
|
| 87 |
+
automatically cancel the request and send the client an error.
|
| 88 |
+
|
| 89 |
+
If `auto_error` is set to `False`, when the query parameter is not
|
| 90 |
+
available, instead of erroring out, the dependency result will be
|
| 91 |
+
`None`.
|
| 92 |
+
|
| 93 |
+
This is useful when you want to have optional authentication.
|
| 94 |
+
|
| 95 |
+
It is also useful when you want to have authentication that can be
|
| 96 |
+
provided in one of multiple optional ways (for example, in a query
|
| 97 |
+
parameter or in an HTTP Bearer token).
|
| 98 |
+
"""
|
| 99 |
+
),
|
| 100 |
+
] = True,
|
| 101 |
+
):
|
| 102 |
+
self.model: APIKey = APIKey(
|
| 103 |
+
**{"in": APIKeyIn.query}, # type: ignore[arg-type]
|
| 104 |
+
name=name,
|
| 105 |
+
description=description,
|
| 106 |
+
)
|
| 107 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 108 |
+
self.auto_error = auto_error
|
| 109 |
+
|
| 110 |
+
async def __call__(self, request: Request) -> Optional[str]:
|
| 111 |
+
api_key = request.query_params.get(self.model.name)
|
| 112 |
+
return self.check_api_key(api_key, self.auto_error)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class APIKeyHeader(APIKeyBase):
|
| 116 |
+
"""
|
| 117 |
+
API key authentication using a header.
|
| 118 |
+
|
| 119 |
+
This defines the name of the header that should be provided in the request with
|
| 120 |
+
the API key and integrates that into the OpenAPI documentation. It extracts
|
| 121 |
+
the key value sent in the header automatically and provides it as the dependency
|
| 122 |
+
result. But it doesn't define how to send that key to the client.
|
| 123 |
+
|
| 124 |
+
## Usage
|
| 125 |
+
|
| 126 |
+
Create an instance object and use that object as the dependency in `Depends()`.
|
| 127 |
+
|
| 128 |
+
The dependency result will be a string containing the key value.
|
| 129 |
+
|
| 130 |
+
## Example
|
| 131 |
+
|
| 132 |
+
```python
|
| 133 |
+
from fastapi import Depends, FastAPI
|
| 134 |
+
from fastapi.security import APIKeyHeader
|
| 135 |
+
|
| 136 |
+
app = FastAPI()
|
| 137 |
+
|
| 138 |
+
header_scheme = APIKeyHeader(name="x-key")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
@app.get("/items/")
|
| 142 |
+
async def read_items(key: str = Depends(header_scheme)):
|
| 143 |
+
return {"key": key}
|
| 144 |
+
```
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
def __init__(
|
| 148 |
+
self,
|
| 149 |
+
*,
|
| 150 |
+
name: Annotated[str, Doc("Header name.")],
|
| 151 |
+
scheme_name: Annotated[
|
| 152 |
+
Optional[str],
|
| 153 |
+
Doc(
|
| 154 |
+
"""
|
| 155 |
+
Security scheme name.
|
| 156 |
+
|
| 157 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 158 |
+
"""
|
| 159 |
+
),
|
| 160 |
+
] = None,
|
| 161 |
+
description: Annotated[
|
| 162 |
+
Optional[str],
|
| 163 |
+
Doc(
|
| 164 |
+
"""
|
| 165 |
+
Security scheme description.
|
| 166 |
+
|
| 167 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 168 |
+
"""
|
| 169 |
+
),
|
| 170 |
+
] = None,
|
| 171 |
+
auto_error: Annotated[
|
| 172 |
+
bool,
|
| 173 |
+
Doc(
|
| 174 |
+
"""
|
| 175 |
+
By default, if the header is not provided, `APIKeyHeader` will
|
| 176 |
+
automatically cancel the request and send the client an error.
|
| 177 |
+
|
| 178 |
+
If `auto_error` is set to `False`, when the header is not available,
|
| 179 |
+
instead of erroring out, the dependency result will be `None`.
|
| 180 |
+
|
| 181 |
+
This is useful when you want to have optional authentication.
|
| 182 |
+
|
| 183 |
+
It is also useful when you want to have authentication that can be
|
| 184 |
+
provided in one of multiple optional ways (for example, in a header or
|
| 185 |
+
in an HTTP Bearer token).
|
| 186 |
+
"""
|
| 187 |
+
),
|
| 188 |
+
] = True,
|
| 189 |
+
):
|
| 190 |
+
self.model: APIKey = APIKey(
|
| 191 |
+
**{"in": APIKeyIn.header}, # type: ignore[arg-type]
|
| 192 |
+
name=name,
|
| 193 |
+
description=description,
|
| 194 |
+
)
|
| 195 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 196 |
+
self.auto_error = auto_error
|
| 197 |
+
|
| 198 |
+
async def __call__(self, request: Request) -> Optional[str]:
|
| 199 |
+
api_key = request.headers.get(self.model.name)
|
| 200 |
+
return self.check_api_key(api_key, self.auto_error)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class APIKeyCookie(APIKeyBase):
|
| 204 |
+
"""
|
| 205 |
+
API key authentication using a cookie.
|
| 206 |
+
|
| 207 |
+
This defines the name of the cookie that should be provided in the request with
|
| 208 |
+
the API key and integrates that into the OpenAPI documentation. It extracts
|
| 209 |
+
the key value sent in the cookie automatically and provides it as the dependency
|
| 210 |
+
result. But it doesn't define how to set that cookie.
|
| 211 |
+
|
| 212 |
+
## Usage
|
| 213 |
+
|
| 214 |
+
Create an instance object and use that object as the dependency in `Depends()`.
|
| 215 |
+
|
| 216 |
+
The dependency result will be a string containing the key value.
|
| 217 |
+
|
| 218 |
+
## Example
|
| 219 |
+
|
| 220 |
+
```python
|
| 221 |
+
from fastapi import Depends, FastAPI
|
| 222 |
+
from fastapi.security import APIKeyCookie
|
| 223 |
+
|
| 224 |
+
app = FastAPI()
|
| 225 |
+
|
| 226 |
+
cookie_scheme = APIKeyCookie(name="session")
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@app.get("/items/")
|
| 230 |
+
async def read_items(session: str = Depends(cookie_scheme)):
|
| 231 |
+
return {"session": session}
|
| 232 |
+
```
|
| 233 |
+
"""
|
| 234 |
+
|
| 235 |
+
def __init__(
|
| 236 |
+
self,
|
| 237 |
+
*,
|
| 238 |
+
name: Annotated[str, Doc("Cookie name.")],
|
| 239 |
+
scheme_name: Annotated[
|
| 240 |
+
Optional[str],
|
| 241 |
+
Doc(
|
| 242 |
+
"""
|
| 243 |
+
Security scheme name.
|
| 244 |
+
|
| 245 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 246 |
+
"""
|
| 247 |
+
),
|
| 248 |
+
] = None,
|
| 249 |
+
description: Annotated[
|
| 250 |
+
Optional[str],
|
| 251 |
+
Doc(
|
| 252 |
+
"""
|
| 253 |
+
Security scheme description.
|
| 254 |
+
|
| 255 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 256 |
+
"""
|
| 257 |
+
),
|
| 258 |
+
] = None,
|
| 259 |
+
auto_error: Annotated[
|
| 260 |
+
bool,
|
| 261 |
+
Doc(
|
| 262 |
+
"""
|
| 263 |
+
By default, if the cookie is not provided, `APIKeyCookie` will
|
| 264 |
+
automatically cancel the request and send the client an error.
|
| 265 |
+
|
| 266 |
+
If `auto_error` is set to `False`, when the cookie is not available,
|
| 267 |
+
instead of erroring out, the dependency result will be `None`.
|
| 268 |
+
|
| 269 |
+
This is useful when you want to have optional authentication.
|
| 270 |
+
|
| 271 |
+
It is also useful when you want to have authentication that can be
|
| 272 |
+
provided in one of multiple optional ways (for example, in a cookie or
|
| 273 |
+
in an HTTP Bearer token).
|
| 274 |
+
"""
|
| 275 |
+
),
|
| 276 |
+
] = True,
|
| 277 |
+
):
|
| 278 |
+
self.model: APIKey = APIKey(
|
| 279 |
+
**{"in": APIKeyIn.cookie}, # type: ignore[arg-type]
|
| 280 |
+
name=name,
|
| 281 |
+
description=description,
|
| 282 |
+
)
|
| 283 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 284 |
+
self.auto_error = auto_error
|
| 285 |
+
|
| 286 |
+
async def __call__(self, request: Request) -> Optional[str]:
|
| 287 |
+
api_key = request.cookies.get(self.model.name)
|
| 288 |
+
return self.check_api_key(api_key, self.auto_error)
|
.venv/lib/python3.11/site-packages/fastapi/security/base.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.openapi.models import SecurityBase as SecurityBaseModel
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class SecurityBase:
|
| 5 |
+
model: SecurityBaseModel
|
| 6 |
+
scheme_name: str
|
.venv/lib/python3.11/site-packages/fastapi/security/http.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import binascii
|
| 2 |
+
from base64 import b64decode
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
from fastapi.exceptions import HTTPException
|
| 6 |
+
from fastapi.openapi.models import HTTPBase as HTTPBaseModel
|
| 7 |
+
from fastapi.openapi.models import HTTPBearer as HTTPBearerModel
|
| 8 |
+
from fastapi.security.base import SecurityBase
|
| 9 |
+
from fastapi.security.utils import get_authorization_scheme_param
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from starlette.requests import Request
|
| 12 |
+
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
| 13 |
+
from typing_extensions import Annotated, Doc
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class HTTPBasicCredentials(BaseModel):
|
| 17 |
+
"""
|
| 18 |
+
The HTTP Basic credentials given as the result of using `HTTPBasic` in a
|
| 19 |
+
dependency.
|
| 20 |
+
|
| 21 |
+
Read more about it in the
|
| 22 |
+
[FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/).
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
username: Annotated[str, Doc("The HTTP Basic username.")]
|
| 26 |
+
password: Annotated[str, Doc("The HTTP Basic password.")]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class HTTPAuthorizationCredentials(BaseModel):
|
| 30 |
+
"""
|
| 31 |
+
The HTTP authorization credentials in the result of using `HTTPBearer` or
|
| 32 |
+
`HTTPDigest` in a dependency.
|
| 33 |
+
|
| 34 |
+
The HTTP authorization header value is split by the first space.
|
| 35 |
+
|
| 36 |
+
The first part is the `scheme`, the second part is the `credentials`.
|
| 37 |
+
|
| 38 |
+
For example, in an HTTP Bearer token scheme, the client will send a header
|
| 39 |
+
like:
|
| 40 |
+
|
| 41 |
+
```
|
| 42 |
+
Authorization: Bearer deadbeef12346
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
In this case:
|
| 46 |
+
|
| 47 |
+
* `scheme` will have the value `"Bearer"`
|
| 48 |
+
* `credentials` will have the value `"deadbeef12346"`
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
scheme: Annotated[
|
| 52 |
+
str,
|
| 53 |
+
Doc(
|
| 54 |
+
"""
|
| 55 |
+
The HTTP authorization scheme extracted from the header value.
|
| 56 |
+
"""
|
| 57 |
+
),
|
| 58 |
+
]
|
| 59 |
+
credentials: Annotated[
|
| 60 |
+
str,
|
| 61 |
+
Doc(
|
| 62 |
+
"""
|
| 63 |
+
The HTTP authorization credentials extracted from the header value.
|
| 64 |
+
"""
|
| 65 |
+
),
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class HTTPBase(SecurityBase):
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
*,
|
| 73 |
+
scheme: str,
|
| 74 |
+
scheme_name: Optional[str] = None,
|
| 75 |
+
description: Optional[str] = None,
|
| 76 |
+
auto_error: bool = True,
|
| 77 |
+
):
|
| 78 |
+
self.model = HTTPBaseModel(scheme=scheme, description=description)
|
| 79 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 80 |
+
self.auto_error = auto_error
|
| 81 |
+
|
| 82 |
+
async def __call__(
|
| 83 |
+
self, request: Request
|
| 84 |
+
) -> Optional[HTTPAuthorizationCredentials]:
|
| 85 |
+
authorization = request.headers.get("Authorization")
|
| 86 |
+
scheme, credentials = get_authorization_scheme_param(authorization)
|
| 87 |
+
if not (authorization and scheme and credentials):
|
| 88 |
+
if self.auto_error:
|
| 89 |
+
raise HTTPException(
|
| 90 |
+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
| 91 |
+
)
|
| 92 |
+
else:
|
| 93 |
+
return None
|
| 94 |
+
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class HTTPBasic(HTTPBase):
|
| 98 |
+
"""
|
| 99 |
+
HTTP Basic authentication.
|
| 100 |
+
|
| 101 |
+
## Usage
|
| 102 |
+
|
| 103 |
+
Create an instance object and use that object as the dependency in `Depends()`.
|
| 104 |
+
|
| 105 |
+
The dependency result will be an `HTTPBasicCredentials` object containing the
|
| 106 |
+
`username` and the `password`.
|
| 107 |
+
|
| 108 |
+
Read more about it in the
|
| 109 |
+
[FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/).
|
| 110 |
+
|
| 111 |
+
## Example
|
| 112 |
+
|
| 113 |
+
```python
|
| 114 |
+
from typing import Annotated
|
| 115 |
+
|
| 116 |
+
from fastapi import Depends, FastAPI
|
| 117 |
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
| 118 |
+
|
| 119 |
+
app = FastAPI()
|
| 120 |
+
|
| 121 |
+
security = HTTPBasic()
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@app.get("/users/me")
|
| 125 |
+
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
|
| 126 |
+
return {"username": credentials.username, "password": credentials.password}
|
| 127 |
+
```
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def __init__(
|
| 131 |
+
self,
|
| 132 |
+
*,
|
| 133 |
+
scheme_name: Annotated[
|
| 134 |
+
Optional[str],
|
| 135 |
+
Doc(
|
| 136 |
+
"""
|
| 137 |
+
Security scheme name.
|
| 138 |
+
|
| 139 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 140 |
+
"""
|
| 141 |
+
),
|
| 142 |
+
] = None,
|
| 143 |
+
realm: Annotated[
|
| 144 |
+
Optional[str],
|
| 145 |
+
Doc(
|
| 146 |
+
"""
|
| 147 |
+
HTTP Basic authentication realm.
|
| 148 |
+
"""
|
| 149 |
+
),
|
| 150 |
+
] = None,
|
| 151 |
+
description: Annotated[
|
| 152 |
+
Optional[str],
|
| 153 |
+
Doc(
|
| 154 |
+
"""
|
| 155 |
+
Security scheme description.
|
| 156 |
+
|
| 157 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 158 |
+
"""
|
| 159 |
+
),
|
| 160 |
+
] = None,
|
| 161 |
+
auto_error: Annotated[
|
| 162 |
+
bool,
|
| 163 |
+
Doc(
|
| 164 |
+
"""
|
| 165 |
+
By default, if the HTTP Basic authentication is not provided (a
|
| 166 |
+
header), `HTTPBasic` will automatically cancel the request and send the
|
| 167 |
+
client an error.
|
| 168 |
+
|
| 169 |
+
If `auto_error` is set to `False`, when the HTTP Basic authentication
|
| 170 |
+
is not available, instead of erroring out, the dependency result will
|
| 171 |
+
be `None`.
|
| 172 |
+
|
| 173 |
+
This is useful when you want to have optional authentication.
|
| 174 |
+
|
| 175 |
+
It is also useful when you want to have authentication that can be
|
| 176 |
+
provided in one of multiple optional ways (for example, in HTTP Basic
|
| 177 |
+
authentication or in an HTTP Bearer token).
|
| 178 |
+
"""
|
| 179 |
+
),
|
| 180 |
+
] = True,
|
| 181 |
+
):
|
| 182 |
+
self.model = HTTPBaseModel(scheme="basic", description=description)
|
| 183 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 184 |
+
self.realm = realm
|
| 185 |
+
self.auto_error = auto_error
|
| 186 |
+
|
| 187 |
+
async def __call__( # type: ignore
|
| 188 |
+
self, request: Request
|
| 189 |
+
) -> Optional[HTTPBasicCredentials]:
|
| 190 |
+
authorization = request.headers.get("Authorization")
|
| 191 |
+
scheme, param = get_authorization_scheme_param(authorization)
|
| 192 |
+
if self.realm:
|
| 193 |
+
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
|
| 194 |
+
else:
|
| 195 |
+
unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
| 196 |
+
if not authorization or scheme.lower() != "basic":
|
| 197 |
+
if self.auto_error:
|
| 198 |
+
raise HTTPException(
|
| 199 |
+
status_code=HTTP_401_UNAUTHORIZED,
|
| 200 |
+
detail="Not authenticated",
|
| 201 |
+
headers=unauthorized_headers,
|
| 202 |
+
)
|
| 203 |
+
else:
|
| 204 |
+
return None
|
| 205 |
+
invalid_user_credentials_exc = HTTPException(
|
| 206 |
+
status_code=HTTP_401_UNAUTHORIZED,
|
| 207 |
+
detail="Invalid authentication credentials",
|
| 208 |
+
headers=unauthorized_headers,
|
| 209 |
+
)
|
| 210 |
+
try:
|
| 211 |
+
data = b64decode(param).decode("ascii")
|
| 212 |
+
except (ValueError, UnicodeDecodeError, binascii.Error):
|
| 213 |
+
raise invalid_user_credentials_exc # noqa: B904
|
| 214 |
+
username, separator, password = data.partition(":")
|
| 215 |
+
if not separator:
|
| 216 |
+
raise invalid_user_credentials_exc
|
| 217 |
+
return HTTPBasicCredentials(username=username, password=password)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class HTTPBearer(HTTPBase):
|
| 221 |
+
"""
|
| 222 |
+
HTTP Bearer token authentication.
|
| 223 |
+
|
| 224 |
+
## Usage
|
| 225 |
+
|
| 226 |
+
Create an instance object and use that object as the dependency in `Depends()`.
|
| 227 |
+
|
| 228 |
+
The dependency result will be an `HTTPAuthorizationCredentials` object containing
|
| 229 |
+
the `scheme` and the `credentials`.
|
| 230 |
+
|
| 231 |
+
## Example
|
| 232 |
+
|
| 233 |
+
```python
|
| 234 |
+
from typing import Annotated
|
| 235 |
+
|
| 236 |
+
from fastapi import Depends, FastAPI
|
| 237 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
| 238 |
+
|
| 239 |
+
app = FastAPI()
|
| 240 |
+
|
| 241 |
+
security = HTTPBearer()
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
@app.get("/users/me")
|
| 245 |
+
def read_current_user(
|
| 246 |
+
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
|
| 247 |
+
):
|
| 248 |
+
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
| 249 |
+
```
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
def __init__(
|
| 253 |
+
self,
|
| 254 |
+
*,
|
| 255 |
+
bearerFormat: Annotated[Optional[str], Doc("Bearer token format.")] = None,
|
| 256 |
+
scheme_name: Annotated[
|
| 257 |
+
Optional[str],
|
| 258 |
+
Doc(
|
| 259 |
+
"""
|
| 260 |
+
Security scheme name.
|
| 261 |
+
|
| 262 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 263 |
+
"""
|
| 264 |
+
),
|
| 265 |
+
] = None,
|
| 266 |
+
description: Annotated[
|
| 267 |
+
Optional[str],
|
| 268 |
+
Doc(
|
| 269 |
+
"""
|
| 270 |
+
Security scheme description.
|
| 271 |
+
|
| 272 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 273 |
+
"""
|
| 274 |
+
),
|
| 275 |
+
] = None,
|
| 276 |
+
auto_error: Annotated[
|
| 277 |
+
bool,
|
| 278 |
+
Doc(
|
| 279 |
+
"""
|
| 280 |
+
By default, if the HTTP Bearer token is not provided (in an
|
| 281 |
+
`Authorization` header), `HTTPBearer` will automatically cancel the
|
| 282 |
+
request and send the client an error.
|
| 283 |
+
|
| 284 |
+
If `auto_error` is set to `False`, when the HTTP Bearer token
|
| 285 |
+
is not available, instead of erroring out, the dependency result will
|
| 286 |
+
be `None`.
|
| 287 |
+
|
| 288 |
+
This is useful when you want to have optional authentication.
|
| 289 |
+
|
| 290 |
+
It is also useful when you want to have authentication that can be
|
| 291 |
+
provided in one of multiple optional ways (for example, in an HTTP
|
| 292 |
+
Bearer token or in a cookie).
|
| 293 |
+
"""
|
| 294 |
+
),
|
| 295 |
+
] = True,
|
| 296 |
+
):
|
| 297 |
+
self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description)
|
| 298 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 299 |
+
self.auto_error = auto_error
|
| 300 |
+
|
| 301 |
+
async def __call__(
|
| 302 |
+
self, request: Request
|
| 303 |
+
) -> Optional[HTTPAuthorizationCredentials]:
|
| 304 |
+
authorization = request.headers.get("Authorization")
|
| 305 |
+
scheme, credentials = get_authorization_scheme_param(authorization)
|
| 306 |
+
if not (authorization and scheme and credentials):
|
| 307 |
+
if self.auto_error:
|
| 308 |
+
raise HTTPException(
|
| 309 |
+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
| 310 |
+
)
|
| 311 |
+
else:
|
| 312 |
+
return None
|
| 313 |
+
if scheme.lower() != "bearer":
|
| 314 |
+
if self.auto_error:
|
| 315 |
+
raise HTTPException(
|
| 316 |
+
status_code=HTTP_403_FORBIDDEN,
|
| 317 |
+
detail="Invalid authentication credentials",
|
| 318 |
+
)
|
| 319 |
+
else:
|
| 320 |
+
return None
|
| 321 |
+
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class HTTPDigest(HTTPBase):
|
| 325 |
+
"""
|
| 326 |
+
HTTP Digest authentication.
|
| 327 |
+
|
| 328 |
+
## Usage
|
| 329 |
+
|
| 330 |
+
Create an instance object and use that object as the dependency in `Depends()`.
|
| 331 |
+
|
| 332 |
+
The dependency result will be an `HTTPAuthorizationCredentials` object containing
|
| 333 |
+
the `scheme` and the `credentials`.
|
| 334 |
+
|
| 335 |
+
## Example
|
| 336 |
+
|
| 337 |
+
```python
|
| 338 |
+
from typing import Annotated
|
| 339 |
+
|
| 340 |
+
from fastapi import Depends, FastAPI
|
| 341 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest
|
| 342 |
+
|
| 343 |
+
app = FastAPI()
|
| 344 |
+
|
| 345 |
+
security = HTTPDigest()
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
@app.get("/users/me")
|
| 349 |
+
def read_current_user(
|
| 350 |
+
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
|
| 351 |
+
):
|
| 352 |
+
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
| 353 |
+
```
|
| 354 |
+
"""
|
| 355 |
+
|
| 356 |
+
def __init__(
|
| 357 |
+
self,
|
| 358 |
+
*,
|
| 359 |
+
scheme_name: Annotated[
|
| 360 |
+
Optional[str],
|
| 361 |
+
Doc(
|
| 362 |
+
"""
|
| 363 |
+
Security scheme name.
|
| 364 |
+
|
| 365 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 366 |
+
"""
|
| 367 |
+
),
|
| 368 |
+
] = None,
|
| 369 |
+
description: Annotated[
|
| 370 |
+
Optional[str],
|
| 371 |
+
Doc(
|
| 372 |
+
"""
|
| 373 |
+
Security scheme description.
|
| 374 |
+
|
| 375 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 376 |
+
"""
|
| 377 |
+
),
|
| 378 |
+
] = None,
|
| 379 |
+
auto_error: Annotated[
|
| 380 |
+
bool,
|
| 381 |
+
Doc(
|
| 382 |
+
"""
|
| 383 |
+
By default, if the HTTP Digest is not provided, `HTTPDigest` will
|
| 384 |
+
automatically cancel the request and send the client an error.
|
| 385 |
+
|
| 386 |
+
If `auto_error` is set to `False`, when the HTTP Digest is not
|
| 387 |
+
available, instead of erroring out, the dependency result will
|
| 388 |
+
be `None`.
|
| 389 |
+
|
| 390 |
+
This is useful when you want to have optional authentication.
|
| 391 |
+
|
| 392 |
+
It is also useful when you want to have authentication that can be
|
| 393 |
+
provided in one of multiple optional ways (for example, in HTTP
|
| 394 |
+
Digest or in a cookie).
|
| 395 |
+
"""
|
| 396 |
+
),
|
| 397 |
+
] = True,
|
| 398 |
+
):
|
| 399 |
+
self.model = HTTPBaseModel(scheme="digest", description=description)
|
| 400 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 401 |
+
self.auto_error = auto_error
|
| 402 |
+
|
| 403 |
+
async def __call__(
|
| 404 |
+
self, request: Request
|
| 405 |
+
) -> Optional[HTTPAuthorizationCredentials]:
|
| 406 |
+
authorization = request.headers.get("Authorization")
|
| 407 |
+
scheme, credentials = get_authorization_scheme_param(authorization)
|
| 408 |
+
if not (authorization and scheme and credentials):
|
| 409 |
+
if self.auto_error:
|
| 410 |
+
raise HTTPException(
|
| 411 |
+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
| 412 |
+
)
|
| 413 |
+
else:
|
| 414 |
+
return None
|
| 415 |
+
if scheme.lower() != "digest":
|
| 416 |
+
raise HTTPException(
|
| 417 |
+
status_code=HTTP_403_FORBIDDEN,
|
| 418 |
+
detail="Invalid authentication credentials",
|
| 419 |
+
)
|
| 420 |
+
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
.venv/lib/python3.11/site-packages/fastapi/security/oauth2.py
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Optional, Union, cast
|
| 2 |
+
|
| 3 |
+
from fastapi.exceptions import HTTPException
|
| 4 |
+
from fastapi.openapi.models import OAuth2 as OAuth2Model
|
| 5 |
+
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
|
| 6 |
+
from fastapi.param_functions import Form
|
| 7 |
+
from fastapi.security.base import SecurityBase
|
| 8 |
+
from fastapi.security.utils import get_authorization_scheme_param
|
| 9 |
+
from starlette.requests import Request
|
| 10 |
+
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
| 11 |
+
|
| 12 |
+
# TODO: import from typing when deprecating Python 3.9
|
| 13 |
+
from typing_extensions import Annotated, Doc
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class OAuth2PasswordRequestForm:
|
| 17 |
+
"""
|
| 18 |
+
This is a dependency class to collect the `username` and `password` as form data
|
| 19 |
+
for an OAuth2 password flow.
|
| 20 |
+
|
| 21 |
+
The OAuth2 specification dictates that for a password flow the data should be
|
| 22 |
+
collected using form data (instead of JSON) and that it should have the specific
|
| 23 |
+
fields `username` and `password`.
|
| 24 |
+
|
| 25 |
+
All the initialization parameters are extracted from the request.
|
| 26 |
+
|
| 27 |
+
Read more about it in the
|
| 28 |
+
[FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
|
| 29 |
+
|
| 30 |
+
## Example
|
| 31 |
+
|
| 32 |
+
```python
|
| 33 |
+
from typing import Annotated
|
| 34 |
+
|
| 35 |
+
from fastapi import Depends, FastAPI
|
| 36 |
+
from fastapi.security import OAuth2PasswordRequestForm
|
| 37 |
+
|
| 38 |
+
app = FastAPI()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@app.post("/login")
|
| 42 |
+
def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
| 43 |
+
data = {}
|
| 44 |
+
data["scopes"] = []
|
| 45 |
+
for scope in form_data.scopes:
|
| 46 |
+
data["scopes"].append(scope)
|
| 47 |
+
if form_data.client_id:
|
| 48 |
+
data["client_id"] = form_data.client_id
|
| 49 |
+
if form_data.client_secret:
|
| 50 |
+
data["client_secret"] = form_data.client_secret
|
| 51 |
+
return data
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
|
| 55 |
+
You could have custom internal logic to separate it by colon characters (`:`) or
|
| 56 |
+
similar, and get the two parts `items` and `read`. Many applications do that to
|
| 57 |
+
group and organize permissions, you could do it as well in your application, just
|
| 58 |
+
know that that it is application specific, it's not part of the specification.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
*,
|
| 64 |
+
grant_type: Annotated[
|
| 65 |
+
Union[str, None],
|
| 66 |
+
Form(pattern="^password$"),
|
| 67 |
+
Doc(
|
| 68 |
+
"""
|
| 69 |
+
The OAuth2 spec says it is required and MUST be the fixed string
|
| 70 |
+
"password". Nevertheless, this dependency class is permissive and
|
| 71 |
+
allows not passing it. If you want to enforce it, use instead the
|
| 72 |
+
`OAuth2PasswordRequestFormStrict` dependency.
|
| 73 |
+
"""
|
| 74 |
+
),
|
| 75 |
+
] = None,
|
| 76 |
+
username: Annotated[
|
| 77 |
+
str,
|
| 78 |
+
Form(),
|
| 79 |
+
Doc(
|
| 80 |
+
"""
|
| 81 |
+
`username` string. The OAuth2 spec requires the exact field name
|
| 82 |
+
`username`.
|
| 83 |
+
"""
|
| 84 |
+
),
|
| 85 |
+
],
|
| 86 |
+
password: Annotated[
|
| 87 |
+
str,
|
| 88 |
+
Form(),
|
| 89 |
+
Doc(
|
| 90 |
+
"""
|
| 91 |
+
`password` string. The OAuth2 spec requires the exact field name
|
| 92 |
+
`password".
|
| 93 |
+
"""
|
| 94 |
+
),
|
| 95 |
+
],
|
| 96 |
+
scope: Annotated[
|
| 97 |
+
str,
|
| 98 |
+
Form(),
|
| 99 |
+
Doc(
|
| 100 |
+
"""
|
| 101 |
+
A single string with actually several scopes separated by spaces. Each
|
| 102 |
+
scope is also a string.
|
| 103 |
+
|
| 104 |
+
For example, a single string with:
|
| 105 |
+
|
| 106 |
+
```python
|
| 107 |
+
"items:read items:write users:read profile openid"
|
| 108 |
+
````
|
| 109 |
+
|
| 110 |
+
would represent the scopes:
|
| 111 |
+
|
| 112 |
+
* `items:read`
|
| 113 |
+
* `items:write`
|
| 114 |
+
* `users:read`
|
| 115 |
+
* `profile`
|
| 116 |
+
* `openid`
|
| 117 |
+
"""
|
| 118 |
+
),
|
| 119 |
+
] = "",
|
| 120 |
+
client_id: Annotated[
|
| 121 |
+
Union[str, None],
|
| 122 |
+
Form(),
|
| 123 |
+
Doc(
|
| 124 |
+
"""
|
| 125 |
+
If there's a `client_id`, it can be sent as part of the form fields.
|
| 126 |
+
But the OAuth2 specification recommends sending the `client_id` and
|
| 127 |
+
`client_secret` (if any) using HTTP Basic auth.
|
| 128 |
+
"""
|
| 129 |
+
),
|
| 130 |
+
] = None,
|
| 131 |
+
client_secret: Annotated[
|
| 132 |
+
Union[str, None],
|
| 133 |
+
Form(),
|
| 134 |
+
Doc(
|
| 135 |
+
"""
|
| 136 |
+
If there's a `client_password` (and a `client_id`), they can be sent
|
| 137 |
+
as part of the form fields. But the OAuth2 specification recommends
|
| 138 |
+
sending the `client_id` and `client_secret` (if any) using HTTP Basic
|
| 139 |
+
auth.
|
| 140 |
+
"""
|
| 141 |
+
),
|
| 142 |
+
] = None,
|
| 143 |
+
):
|
| 144 |
+
self.grant_type = grant_type
|
| 145 |
+
self.username = username
|
| 146 |
+
self.password = password
|
| 147 |
+
self.scopes = scope.split()
|
| 148 |
+
self.client_id = client_id
|
| 149 |
+
self.client_secret = client_secret
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
| 153 |
+
"""
|
| 154 |
+
This is a dependency class to collect the `username` and `password` as form data
|
| 155 |
+
for an OAuth2 password flow.
|
| 156 |
+
|
| 157 |
+
The OAuth2 specification dictates that for a password flow the data should be
|
| 158 |
+
collected using form data (instead of JSON) and that it should have the specific
|
| 159 |
+
fields `username` and `password`.
|
| 160 |
+
|
| 161 |
+
All the initialization parameters are extracted from the request.
|
| 162 |
+
|
| 163 |
+
The only difference between `OAuth2PasswordRequestFormStrict` and
|
| 164 |
+
`OAuth2PasswordRequestForm` is that `OAuth2PasswordRequestFormStrict` requires the
|
| 165 |
+
client to send the form field `grant_type` with the value `"password"`, which
|
| 166 |
+
is required in the OAuth2 specification (it seems that for no particular reason),
|
| 167 |
+
while for `OAuth2PasswordRequestForm` `grant_type` is optional.
|
| 168 |
+
|
| 169 |
+
Read more about it in the
|
| 170 |
+
[FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
|
| 171 |
+
|
| 172 |
+
## Example
|
| 173 |
+
|
| 174 |
+
```python
|
| 175 |
+
from typing import Annotated
|
| 176 |
+
|
| 177 |
+
from fastapi import Depends, FastAPI
|
| 178 |
+
from fastapi.security import OAuth2PasswordRequestForm
|
| 179 |
+
|
| 180 |
+
app = FastAPI()
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@app.post("/login")
|
| 184 |
+
def login(form_data: Annotated[OAuth2PasswordRequestFormStrict, Depends()]):
|
| 185 |
+
data = {}
|
| 186 |
+
data["scopes"] = []
|
| 187 |
+
for scope in form_data.scopes:
|
| 188 |
+
data["scopes"].append(scope)
|
| 189 |
+
if form_data.client_id:
|
| 190 |
+
data["client_id"] = form_data.client_id
|
| 191 |
+
if form_data.client_secret:
|
| 192 |
+
data["client_secret"] = form_data.client_secret
|
| 193 |
+
return data
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
|
| 197 |
+
You could have custom internal logic to separate it by colon characters (`:`) or
|
| 198 |
+
similar, and get the two parts `items` and `read`. Many applications do that to
|
| 199 |
+
group and organize permissions, you could do it as well in your application, just
|
| 200 |
+
know that that it is application specific, it's not part of the specification.
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
|
| 204 |
+
This dependency is strict about it. If you want to be permissive, use instead the
|
| 205 |
+
OAuth2PasswordRequestForm dependency class.
|
| 206 |
+
username: username string. The OAuth2 spec requires the exact field name "username".
|
| 207 |
+
password: password string. The OAuth2 spec requires the exact field name "password".
|
| 208 |
+
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
|
| 209 |
+
"items:read items:write users:read profile openid"
|
| 210 |
+
client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
| 211 |
+
using HTTP Basic auth, as: client_id:client_secret
|
| 212 |
+
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
| 213 |
+
using HTTP Basic auth, as: client_id:client_secret
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
def __init__(
|
| 217 |
+
self,
|
| 218 |
+
grant_type: Annotated[
|
| 219 |
+
str,
|
| 220 |
+
Form(pattern="^password$"),
|
| 221 |
+
Doc(
|
| 222 |
+
"""
|
| 223 |
+
The OAuth2 spec says it is required and MUST be the fixed string
|
| 224 |
+
"password". This dependency is strict about it. If you want to be
|
| 225 |
+
permissive, use instead the `OAuth2PasswordRequestForm` dependency
|
| 226 |
+
class.
|
| 227 |
+
"""
|
| 228 |
+
),
|
| 229 |
+
],
|
| 230 |
+
username: Annotated[
|
| 231 |
+
str,
|
| 232 |
+
Form(),
|
| 233 |
+
Doc(
|
| 234 |
+
"""
|
| 235 |
+
`username` string. The OAuth2 spec requires the exact field name
|
| 236 |
+
`username`.
|
| 237 |
+
"""
|
| 238 |
+
),
|
| 239 |
+
],
|
| 240 |
+
password: Annotated[
|
| 241 |
+
str,
|
| 242 |
+
Form(),
|
| 243 |
+
Doc(
|
| 244 |
+
"""
|
| 245 |
+
`password` string. The OAuth2 spec requires the exact field name
|
| 246 |
+
`password".
|
| 247 |
+
"""
|
| 248 |
+
),
|
| 249 |
+
],
|
| 250 |
+
scope: Annotated[
|
| 251 |
+
str,
|
| 252 |
+
Form(),
|
| 253 |
+
Doc(
|
| 254 |
+
"""
|
| 255 |
+
A single string with actually several scopes separated by spaces. Each
|
| 256 |
+
scope is also a string.
|
| 257 |
+
|
| 258 |
+
For example, a single string with:
|
| 259 |
+
|
| 260 |
+
```python
|
| 261 |
+
"items:read items:write users:read profile openid"
|
| 262 |
+
````
|
| 263 |
+
|
| 264 |
+
would represent the scopes:
|
| 265 |
+
|
| 266 |
+
* `items:read`
|
| 267 |
+
* `items:write`
|
| 268 |
+
* `users:read`
|
| 269 |
+
* `profile`
|
| 270 |
+
* `openid`
|
| 271 |
+
"""
|
| 272 |
+
),
|
| 273 |
+
] = "",
|
| 274 |
+
client_id: Annotated[
|
| 275 |
+
Union[str, None],
|
| 276 |
+
Form(),
|
| 277 |
+
Doc(
|
| 278 |
+
"""
|
| 279 |
+
If there's a `client_id`, it can be sent as part of the form fields.
|
| 280 |
+
But the OAuth2 specification recommends sending the `client_id` and
|
| 281 |
+
`client_secret` (if any) using HTTP Basic auth.
|
| 282 |
+
"""
|
| 283 |
+
),
|
| 284 |
+
] = None,
|
| 285 |
+
client_secret: Annotated[
|
| 286 |
+
Union[str, None],
|
| 287 |
+
Form(),
|
| 288 |
+
Doc(
|
| 289 |
+
"""
|
| 290 |
+
If there's a `client_password` (and a `client_id`), they can be sent
|
| 291 |
+
as part of the form fields. But the OAuth2 specification recommends
|
| 292 |
+
sending the `client_id` and `client_secret` (if any) using HTTP Basic
|
| 293 |
+
auth.
|
| 294 |
+
"""
|
| 295 |
+
),
|
| 296 |
+
] = None,
|
| 297 |
+
):
|
| 298 |
+
super().__init__(
|
| 299 |
+
grant_type=grant_type,
|
| 300 |
+
username=username,
|
| 301 |
+
password=password,
|
| 302 |
+
scope=scope,
|
| 303 |
+
client_id=client_id,
|
| 304 |
+
client_secret=client_secret,
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class OAuth2(SecurityBase):
|
| 309 |
+
"""
|
| 310 |
+
This is the base class for OAuth2 authentication, an instance of it would be used
|
| 311 |
+
as a dependency. All other OAuth2 classes inherit from it and customize it for
|
| 312 |
+
each OAuth2 flow.
|
| 313 |
+
|
| 314 |
+
You normally would not create a new class inheriting from it but use one of the
|
| 315 |
+
existing subclasses, and maybe compose them if you want to support multiple flows.
|
| 316 |
+
|
| 317 |
+
Read more about it in the
|
| 318 |
+
[FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/).
|
| 319 |
+
"""
|
| 320 |
+
|
| 321 |
+
def __init__(
|
| 322 |
+
self,
|
| 323 |
+
*,
|
| 324 |
+
flows: Annotated[
|
| 325 |
+
Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]],
|
| 326 |
+
Doc(
|
| 327 |
+
"""
|
| 328 |
+
The dictionary of OAuth2 flows.
|
| 329 |
+
"""
|
| 330 |
+
),
|
| 331 |
+
] = OAuthFlowsModel(),
|
| 332 |
+
scheme_name: Annotated[
|
| 333 |
+
Optional[str],
|
| 334 |
+
Doc(
|
| 335 |
+
"""
|
| 336 |
+
Security scheme name.
|
| 337 |
+
|
| 338 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 339 |
+
"""
|
| 340 |
+
),
|
| 341 |
+
] = None,
|
| 342 |
+
description: Annotated[
|
| 343 |
+
Optional[str],
|
| 344 |
+
Doc(
|
| 345 |
+
"""
|
| 346 |
+
Security scheme description.
|
| 347 |
+
|
| 348 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 349 |
+
"""
|
| 350 |
+
),
|
| 351 |
+
] = None,
|
| 352 |
+
auto_error: Annotated[
|
| 353 |
+
bool,
|
| 354 |
+
Doc(
|
| 355 |
+
"""
|
| 356 |
+
By default, if no HTTP Authorization header is provided, required for
|
| 357 |
+
OAuth2 authentication, it will automatically cancel the request and
|
| 358 |
+
send the client an error.
|
| 359 |
+
|
| 360 |
+
If `auto_error` is set to `False`, when the HTTP Authorization header
|
| 361 |
+
is not available, instead of erroring out, the dependency result will
|
| 362 |
+
be `None`.
|
| 363 |
+
|
| 364 |
+
This is useful when you want to have optional authentication.
|
| 365 |
+
|
| 366 |
+
It is also useful when you want to have authentication that can be
|
| 367 |
+
provided in one of multiple optional ways (for example, with OAuth2
|
| 368 |
+
or in a cookie).
|
| 369 |
+
"""
|
| 370 |
+
),
|
| 371 |
+
] = True,
|
| 372 |
+
):
|
| 373 |
+
self.model = OAuth2Model(
|
| 374 |
+
flows=cast(OAuthFlowsModel, flows), description=description
|
| 375 |
+
)
|
| 376 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 377 |
+
self.auto_error = auto_error
|
| 378 |
+
|
| 379 |
+
async def __call__(self, request: Request) -> Optional[str]:
|
| 380 |
+
authorization = request.headers.get("Authorization")
|
| 381 |
+
if not authorization:
|
| 382 |
+
if self.auto_error:
|
| 383 |
+
raise HTTPException(
|
| 384 |
+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
| 385 |
+
)
|
| 386 |
+
else:
|
| 387 |
+
return None
|
| 388 |
+
return authorization
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
class OAuth2PasswordBearer(OAuth2):
|
| 392 |
+
"""
|
| 393 |
+
OAuth2 flow for authentication using a bearer token obtained with a password.
|
| 394 |
+
An instance of it would be used as a dependency.
|
| 395 |
+
|
| 396 |
+
Read more about it in the
|
| 397 |
+
[FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
|
| 398 |
+
"""
|
| 399 |
+
|
| 400 |
+
def __init__(
|
| 401 |
+
self,
|
| 402 |
+
tokenUrl: Annotated[
|
| 403 |
+
str,
|
| 404 |
+
Doc(
|
| 405 |
+
"""
|
| 406 |
+
The URL to obtain the OAuth2 token. This would be the *path operation*
|
| 407 |
+
that has `OAuth2PasswordRequestForm` as a dependency.
|
| 408 |
+
"""
|
| 409 |
+
),
|
| 410 |
+
],
|
| 411 |
+
scheme_name: Annotated[
|
| 412 |
+
Optional[str],
|
| 413 |
+
Doc(
|
| 414 |
+
"""
|
| 415 |
+
Security scheme name.
|
| 416 |
+
|
| 417 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 418 |
+
"""
|
| 419 |
+
),
|
| 420 |
+
] = None,
|
| 421 |
+
scopes: Annotated[
|
| 422 |
+
Optional[Dict[str, str]],
|
| 423 |
+
Doc(
|
| 424 |
+
"""
|
| 425 |
+
The OAuth2 scopes that would be required by the *path operations* that
|
| 426 |
+
use this dependency.
|
| 427 |
+
"""
|
| 428 |
+
),
|
| 429 |
+
] = None,
|
| 430 |
+
description: Annotated[
|
| 431 |
+
Optional[str],
|
| 432 |
+
Doc(
|
| 433 |
+
"""
|
| 434 |
+
Security scheme description.
|
| 435 |
+
|
| 436 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 437 |
+
"""
|
| 438 |
+
),
|
| 439 |
+
] = None,
|
| 440 |
+
auto_error: Annotated[
|
| 441 |
+
bool,
|
| 442 |
+
Doc(
|
| 443 |
+
"""
|
| 444 |
+
By default, if no HTTP Authorization header is provided, required for
|
| 445 |
+
OAuth2 authentication, it will automatically cancel the request and
|
| 446 |
+
send the client an error.
|
| 447 |
+
|
| 448 |
+
If `auto_error` is set to `False`, when the HTTP Authorization header
|
| 449 |
+
is not available, instead of erroring out, the dependency result will
|
| 450 |
+
be `None`.
|
| 451 |
+
|
| 452 |
+
This is useful when you want to have optional authentication.
|
| 453 |
+
|
| 454 |
+
It is also useful when you want to have authentication that can be
|
| 455 |
+
provided in one of multiple optional ways (for example, with OAuth2
|
| 456 |
+
or in a cookie).
|
| 457 |
+
"""
|
| 458 |
+
),
|
| 459 |
+
] = True,
|
| 460 |
+
):
|
| 461 |
+
if not scopes:
|
| 462 |
+
scopes = {}
|
| 463 |
+
flows = OAuthFlowsModel(
|
| 464 |
+
password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes})
|
| 465 |
+
)
|
| 466 |
+
super().__init__(
|
| 467 |
+
flows=flows,
|
| 468 |
+
scheme_name=scheme_name,
|
| 469 |
+
description=description,
|
| 470 |
+
auto_error=auto_error,
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
async def __call__(self, request: Request) -> Optional[str]:
|
| 474 |
+
authorization = request.headers.get("Authorization")
|
| 475 |
+
scheme, param = get_authorization_scheme_param(authorization)
|
| 476 |
+
if not authorization or scheme.lower() != "bearer":
|
| 477 |
+
if self.auto_error:
|
| 478 |
+
raise HTTPException(
|
| 479 |
+
status_code=HTTP_401_UNAUTHORIZED,
|
| 480 |
+
detail="Not authenticated",
|
| 481 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 482 |
+
)
|
| 483 |
+
else:
|
| 484 |
+
return None
|
| 485 |
+
return param
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
class OAuth2AuthorizationCodeBearer(OAuth2):
|
| 489 |
+
"""
|
| 490 |
+
OAuth2 flow for authentication using a bearer token obtained with an OAuth2 code
|
| 491 |
+
flow. An instance of it would be used as a dependency.
|
| 492 |
+
"""
|
| 493 |
+
|
| 494 |
+
def __init__(
|
| 495 |
+
self,
|
| 496 |
+
authorizationUrl: str,
|
| 497 |
+
tokenUrl: Annotated[
|
| 498 |
+
str,
|
| 499 |
+
Doc(
|
| 500 |
+
"""
|
| 501 |
+
The URL to obtain the OAuth2 token.
|
| 502 |
+
"""
|
| 503 |
+
),
|
| 504 |
+
],
|
| 505 |
+
refreshUrl: Annotated[
|
| 506 |
+
Optional[str],
|
| 507 |
+
Doc(
|
| 508 |
+
"""
|
| 509 |
+
The URL to refresh the token and obtain a new one.
|
| 510 |
+
"""
|
| 511 |
+
),
|
| 512 |
+
] = None,
|
| 513 |
+
scheme_name: Annotated[
|
| 514 |
+
Optional[str],
|
| 515 |
+
Doc(
|
| 516 |
+
"""
|
| 517 |
+
Security scheme name.
|
| 518 |
+
|
| 519 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 520 |
+
"""
|
| 521 |
+
),
|
| 522 |
+
] = None,
|
| 523 |
+
scopes: Annotated[
|
| 524 |
+
Optional[Dict[str, str]],
|
| 525 |
+
Doc(
|
| 526 |
+
"""
|
| 527 |
+
The OAuth2 scopes that would be required by the *path operations* that
|
| 528 |
+
use this dependency.
|
| 529 |
+
"""
|
| 530 |
+
),
|
| 531 |
+
] = None,
|
| 532 |
+
description: Annotated[
|
| 533 |
+
Optional[str],
|
| 534 |
+
Doc(
|
| 535 |
+
"""
|
| 536 |
+
Security scheme description.
|
| 537 |
+
|
| 538 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 539 |
+
"""
|
| 540 |
+
),
|
| 541 |
+
] = None,
|
| 542 |
+
auto_error: Annotated[
|
| 543 |
+
bool,
|
| 544 |
+
Doc(
|
| 545 |
+
"""
|
| 546 |
+
By default, if no HTTP Authorization header is provided, required for
|
| 547 |
+
OAuth2 authentication, it will automatically cancel the request and
|
| 548 |
+
send the client an error.
|
| 549 |
+
|
| 550 |
+
If `auto_error` is set to `False`, when the HTTP Authorization header
|
| 551 |
+
is not available, instead of erroring out, the dependency result will
|
| 552 |
+
be `None`.
|
| 553 |
+
|
| 554 |
+
This is useful when you want to have optional authentication.
|
| 555 |
+
|
| 556 |
+
It is also useful when you want to have authentication that can be
|
| 557 |
+
provided in one of multiple optional ways (for example, with OAuth2
|
| 558 |
+
or in a cookie).
|
| 559 |
+
"""
|
| 560 |
+
),
|
| 561 |
+
] = True,
|
| 562 |
+
):
|
| 563 |
+
if not scopes:
|
| 564 |
+
scopes = {}
|
| 565 |
+
flows = OAuthFlowsModel(
|
| 566 |
+
authorizationCode=cast(
|
| 567 |
+
Any,
|
| 568 |
+
{
|
| 569 |
+
"authorizationUrl": authorizationUrl,
|
| 570 |
+
"tokenUrl": tokenUrl,
|
| 571 |
+
"refreshUrl": refreshUrl,
|
| 572 |
+
"scopes": scopes,
|
| 573 |
+
},
|
| 574 |
+
)
|
| 575 |
+
)
|
| 576 |
+
super().__init__(
|
| 577 |
+
flows=flows,
|
| 578 |
+
scheme_name=scheme_name,
|
| 579 |
+
description=description,
|
| 580 |
+
auto_error=auto_error,
|
| 581 |
+
)
|
| 582 |
+
|
| 583 |
+
async def __call__(self, request: Request) -> Optional[str]:
|
| 584 |
+
authorization = request.headers.get("Authorization")
|
| 585 |
+
scheme, param = get_authorization_scheme_param(authorization)
|
| 586 |
+
if not authorization or scheme.lower() != "bearer":
|
| 587 |
+
if self.auto_error:
|
| 588 |
+
raise HTTPException(
|
| 589 |
+
status_code=HTTP_401_UNAUTHORIZED,
|
| 590 |
+
detail="Not authenticated",
|
| 591 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 592 |
+
)
|
| 593 |
+
else:
|
| 594 |
+
return None # pragma: nocover
|
| 595 |
+
return param
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
class SecurityScopes:
|
| 599 |
+
"""
|
| 600 |
+
This is a special class that you can define in a parameter in a dependency to
|
| 601 |
+
obtain the OAuth2 scopes required by all the dependencies in the same chain.
|
| 602 |
+
|
| 603 |
+
This way, multiple dependencies can have different scopes, even when used in the
|
| 604 |
+
same *path operation*. And with this, you can access all the scopes required in
|
| 605 |
+
all those dependencies in a single place.
|
| 606 |
+
|
| 607 |
+
Read more about it in the
|
| 608 |
+
[FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/).
|
| 609 |
+
"""
|
| 610 |
+
|
| 611 |
+
def __init__(
|
| 612 |
+
self,
|
| 613 |
+
scopes: Annotated[
|
| 614 |
+
Optional[List[str]],
|
| 615 |
+
Doc(
|
| 616 |
+
"""
|
| 617 |
+
This will be filled by FastAPI.
|
| 618 |
+
"""
|
| 619 |
+
),
|
| 620 |
+
] = None,
|
| 621 |
+
):
|
| 622 |
+
self.scopes: Annotated[
|
| 623 |
+
List[str],
|
| 624 |
+
Doc(
|
| 625 |
+
"""
|
| 626 |
+
The list of all the scopes required by dependencies.
|
| 627 |
+
"""
|
| 628 |
+
),
|
| 629 |
+
] = scopes or []
|
| 630 |
+
self.scope_str: Annotated[
|
| 631 |
+
str,
|
| 632 |
+
Doc(
|
| 633 |
+
"""
|
| 634 |
+
All the scopes required by all the dependencies in a single string
|
| 635 |
+
separated by spaces, as defined in the OAuth2 specification.
|
| 636 |
+
"""
|
| 637 |
+
),
|
| 638 |
+
] = " ".join(self.scopes)
|
.venv/lib/python3.11/site-packages/fastapi/security/open_id_connect_url.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
|
| 4 |
+
from fastapi.security.base import SecurityBase
|
| 5 |
+
from starlette.exceptions import HTTPException
|
| 6 |
+
from starlette.requests import Request
|
| 7 |
+
from starlette.status import HTTP_403_FORBIDDEN
|
| 8 |
+
from typing_extensions import Annotated, Doc
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class OpenIdConnect(SecurityBase):
|
| 12 |
+
"""
|
| 13 |
+
OpenID Connect authentication class. An instance of it would be used as a
|
| 14 |
+
dependency.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
*,
|
| 20 |
+
openIdConnectUrl: Annotated[
|
| 21 |
+
str,
|
| 22 |
+
Doc(
|
| 23 |
+
"""
|
| 24 |
+
The OpenID Connect URL.
|
| 25 |
+
"""
|
| 26 |
+
),
|
| 27 |
+
],
|
| 28 |
+
scheme_name: Annotated[
|
| 29 |
+
Optional[str],
|
| 30 |
+
Doc(
|
| 31 |
+
"""
|
| 32 |
+
Security scheme name.
|
| 33 |
+
|
| 34 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 35 |
+
"""
|
| 36 |
+
),
|
| 37 |
+
] = None,
|
| 38 |
+
description: Annotated[
|
| 39 |
+
Optional[str],
|
| 40 |
+
Doc(
|
| 41 |
+
"""
|
| 42 |
+
Security scheme description.
|
| 43 |
+
|
| 44 |
+
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
| 45 |
+
"""
|
| 46 |
+
),
|
| 47 |
+
] = None,
|
| 48 |
+
auto_error: Annotated[
|
| 49 |
+
bool,
|
| 50 |
+
Doc(
|
| 51 |
+
"""
|
| 52 |
+
By default, if no HTTP Authorization header is provided, required for
|
| 53 |
+
OpenID Connect authentication, it will automatically cancel the request
|
| 54 |
+
and send the client an error.
|
| 55 |
+
|
| 56 |
+
If `auto_error` is set to `False`, when the HTTP Authorization header
|
| 57 |
+
is not available, instead of erroring out, the dependency result will
|
| 58 |
+
be `None`.
|
| 59 |
+
|
| 60 |
+
This is useful when you want to have optional authentication.
|
| 61 |
+
|
| 62 |
+
It is also useful when you want to have authentication that can be
|
| 63 |
+
provided in one of multiple optional ways (for example, with OpenID
|
| 64 |
+
Connect or in a cookie).
|
| 65 |
+
"""
|
| 66 |
+
),
|
| 67 |
+
] = True,
|
| 68 |
+
):
|
| 69 |
+
self.model = OpenIdConnectModel(
|
| 70 |
+
openIdConnectUrl=openIdConnectUrl, description=description
|
| 71 |
+
)
|
| 72 |
+
self.scheme_name = scheme_name or self.__class__.__name__
|
| 73 |
+
self.auto_error = auto_error
|
| 74 |
+
|
| 75 |
+
async def __call__(self, request: Request) -> Optional[str]:
|
| 76 |
+
authorization = request.headers.get("Authorization")
|
| 77 |
+
if not authorization:
|
| 78 |
+
if self.auto_error:
|
| 79 |
+
raise HTTPException(
|
| 80 |
+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
| 81 |
+
)
|
| 82 |
+
else:
|
| 83 |
+
return None
|
| 84 |
+
return authorization
|
.venv/lib/python3.11/site-packages/fastapi/security/utils.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, Tuple
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def get_authorization_scheme_param(
|
| 5 |
+
authorization_header_value: Optional[str],
|
| 6 |
+
) -> Tuple[str, str]:
|
| 7 |
+
if not authorization_header_value:
|
| 8 |
+
return "", ""
|
| 9 |
+
scheme, _, param = authorization_header_value.partition(" ")
|
| 10 |
+
return scheme, param
|
.venv/lib/python3.11/site-packages/partial_json_parser/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .core.api import JSON, ensure_json, parse_json
|
| 2 |
+
from .core.complete import fix
|
| 3 |
+
from .core.exceptions import *
|
| 4 |
+
from .core.myelin import fix_fast
|
| 5 |
+
from .core.options import *
|
| 6 |
+
|
| 7 |
+
loads = decode = parse_json
|
.venv/lib/python3.11/site-packages/partial_json_parser/__pycache__/version.cpython-311.pyc
ADDED
|
Binary file (219 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/api.cpython-311.pyc
ADDED
|
Binary file (1.58 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/complete.cpython-311.pyc
ADDED
|
Binary file (9.55 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/exceptions.cpython-311.pyc
ADDED
|
Binary file (802 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/myelin.cpython-311.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/partial_json_parser/core/__pycache__/options.cpython-311.pyc
ADDED
|
Binary file (1.56 kB). View file
|
|
|