fastapi_swagger2 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ __version__ = "0.0.1"
2
+
3
+ from typing import Any, Dict, List, Optional, TypeVar
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.openapi.docs import (
7
+ get_redoc_html,
8
+ get_swagger_ui_html,
9
+ get_swagger_ui_oauth2_redirect_html,
10
+ )
11
+ from starlette.requests import Request
12
+ from starlette.responses import HTMLResponse, JSONResponse
13
+
14
+ from fastapi_swagger2.utils import get_swagger2
15
+
16
+
17
+ # Keep mypy happy with the monkey patching
18
+ class FastAPIEx(FastAPI):
19
+ swagger2_url: Optional[str]
20
+ swagger2_tags: Optional[List[Dict[str, Any]]]
21
+ swagger2_docs_url: Optional[str]
22
+ swagger2_redoc_url: Optional[str]
23
+ swagger2_ui_oauth2_redirect_url: Optional[str]
24
+ swagger2_ui_init_oauth: Optional[Dict[str, Any]]
25
+ swagger2_ui_parameters: Optional[Dict[str, Any]]
26
+ swagger2_version: str = "2.0"
27
+ swagger2_schema: Optional[Dict[str, Any]]
28
+
29
+ swagger2: Any
30
+
31
+
32
+ AppType = TypeVar("AppType", bound="FastAPIEx")
33
+
34
+
35
+ class FastAPISwagger2:
36
+ def __init__(
37
+ self,
38
+ app: AppType,
39
+ swagger2_url: Optional[str] = "/swagger2.json",
40
+ swagger2_tags: Optional[List[Dict[str, Any]]] = None,
41
+ swagger2_docs_url: Optional[str] = "/swagger2/docs",
42
+ swagger2_redoc_url: Optional[str] = "/swagger2/redoc",
43
+ swagger2_ui_oauth2_redirect_url: Optional[
44
+ str
45
+ ] = "/swagger2/docs/oauth2-redirect",
46
+ swagger2_ui_init_oauth: Optional[Dict[str, Any]] = None,
47
+ swagger2_ui_parameters: Optional[Dict[str, Any]] = None,
48
+ ) -> None:
49
+ self.app = app
50
+ self.app.swagger2_url = swagger2_url
51
+ self.app.swagger2_tags = swagger2_tags
52
+ self.app.swagger2_docs_url = swagger2_docs_url
53
+ self.app.swagger2_redoc_url = swagger2_redoc_url
54
+ self.app.swagger2_ui_oauth2_redirect_url = swagger2_ui_oauth2_redirect_url
55
+ self.app.swagger2_ui_init_oauth = swagger2_ui_init_oauth
56
+ self.app.swagger2_ui_parameters = swagger2_ui_parameters
57
+
58
+ self.app.swagger2_version = "2.0"
59
+ self.app.swagger2_schema = None
60
+ if self.app.swagger2_url:
61
+ assert (
62
+ self.app.title
63
+ ), "A title must be provided for Swagger, e.g.: 'My API'"
64
+ assert (
65
+ self.app.version
66
+ ), "A version must be provided for Swagger, e.g.: '2.1.0'"
67
+
68
+ self.app.swagger2 = self.swagger2
69
+
70
+ self.setup()
71
+
72
+ def setup(self) -> None:
73
+ if self.app.swagger2_url:
74
+ urls = (server_data.get("url") for server_data in self.app.servers)
75
+ server_urls = {url for url in urls if url}
76
+
77
+ async def swagger2(req: Request) -> JSONResponse:
78
+ root_path = req.scope.get("root_path", "").rstrip("/")
79
+ if root_path not in server_urls:
80
+ if root_path and self.app.root_path_in_servers:
81
+ self.app.servers.insert(0, {"url": root_path})
82
+ server_urls.add(root_path)
83
+ return JSONResponse(self.swagger2())
84
+
85
+ self.app.add_route(self.app.swagger2_url, swagger2, include_in_schema=False)
86
+
87
+ if self.app.swagger2_url and self.app.swagger2_docs_url:
88
+
89
+ async def swagger_ui_html(req: Request) -> HTMLResponse:
90
+ root_path = req.scope.get("root_path", "").rstrip("/")
91
+ swagger2_url = root_path + self.app.swagger2_url
92
+ oauth2_redirect_url = self.app.swagger2_ui_oauth2_redirect_url
93
+ if oauth2_redirect_url:
94
+ oauth2_redirect_url = root_path + oauth2_redirect_url
95
+ return get_swagger_ui_html(
96
+ openapi_url=swagger2_url,
97
+ title=self.app.title + " - Swagger UI",
98
+ oauth2_redirect_url=oauth2_redirect_url,
99
+ init_oauth=self.app.swagger2_ui_init_oauth,
100
+ swagger_ui_parameters=self.app.swagger2_ui_parameters,
101
+ )
102
+
103
+ self.app.add_route(
104
+ self.app.swagger2_docs_url, swagger_ui_html, include_in_schema=False
105
+ )
106
+
107
+ if self.app.swagger2_ui_oauth2_redirect_url:
108
+
109
+ async def swagger_ui_redirect(req: Request) -> HTMLResponse:
110
+ return get_swagger_ui_oauth2_redirect_html()
111
+
112
+ self.app.add_route(
113
+ self.app.swagger2_ui_oauth2_redirect_url,
114
+ swagger_ui_redirect,
115
+ include_in_schema=False,
116
+ )
117
+ if self.app.swagger2_url and self.app.swagger2_redoc_url:
118
+
119
+ async def redoc_html(req: Request) -> HTMLResponse:
120
+ root_path = req.scope.get("root_path", "").rstrip("/")
121
+ swagger2_url = root_path + self.app.swagger2_url
122
+ return get_redoc_html(
123
+ openapi_url=swagger2_url, title=self.app.title + " - ReDoc"
124
+ )
125
+
126
+ self.app.add_route(
127
+ self.app.swagger2_redoc_url, redoc_html, include_in_schema=False
128
+ )
129
+
130
+ def swagger2(self) -> Dict[str, Any]:
131
+ if not self.app.swagger2_schema:
132
+ self.app.swagger2_schema = get_swagger2(
133
+ title=self.app.title,
134
+ version=self.app.version,
135
+ swagger2_version=self.app.swagger2_version,
136
+ description=self.app.description,
137
+ server=str(self.app.servers[0]) if self.app.servers else None,
138
+ terms_of_service=self.app.terms_of_service,
139
+ contact=self.app.contact,
140
+ license_info=self.app.license_info,
141
+ routes=self.app.routes,
142
+ tags=self.app.swagger2_tags,
143
+ )
144
+
145
+ return self.app.swagger2_schema
@@ -0,0 +1 @@
1
+ REF_PREFIX = "#/definitions/"
@@ -0,0 +1,353 @@
1
+ from enum import Enum
2
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Union
3
+
4
+ from fastapi.logger import logger
5
+ from pydantic import AnyUrl, BaseModel, Field
6
+
7
+ try:
8
+ import email_validator # type: ignore
9
+
10
+ assert email_validator # make autoflake ignore the unused import
11
+ from pydantic import EmailStr
12
+ except ImportError: # pragma: no cover
13
+
14
+ class EmailStr(str): # type: ignore
15
+ @classmethod
16
+ def __get_validators__(cls) -> Iterable[Callable[..., Any]]:
17
+ yield cls.validate
18
+
19
+ @classmethod
20
+ def validate(cls, v: Any) -> str:
21
+ logger.warning(
22
+ "email-validator not installed, email fields will be treated as str.\n"
23
+ "To install, run: pip install email-validator"
24
+ )
25
+ return str(v)
26
+
27
+
28
+ class Contact(BaseModel):
29
+ name: Optional[str] = None
30
+ url: Optional[AnyUrl] = None
31
+ email: Optional[EmailStr] = None
32
+
33
+ class Config:
34
+ extra = "allow"
35
+
36
+
37
+ class License(BaseModel):
38
+ name: str
39
+ url: Optional[AnyUrl] = None
40
+
41
+ class Config:
42
+ extra = "allow"
43
+
44
+
45
+ class Info(BaseModel):
46
+ title: str
47
+ description: Optional[str] = None
48
+ termsOfService: Optional[str] = None
49
+ contact: Optional[Contact] = None
50
+ license: Optional[License] = None
51
+ version: str
52
+
53
+ class Config:
54
+ extra = "allow"
55
+
56
+
57
+ # class URLHost(Field)
58
+ """
59
+ The host (name or ip) serving the API. This MUST be the host only and does not include the scheme nor sub-paths. It
60
+ MAY include a port. If the host is not included, the host serving the documentation is to be used (including the port)
61
+ """
62
+
63
+ # class URLBasePath(Field)
64
+ """
65
+ The base path on which the API is served, which is relative to the host. If it is not included, the API is served
66
+ directly under the host. The value MUST start with a leading slash (/).
67
+ """
68
+
69
+
70
+ class URLSchemeEnum(Enum):
71
+ http = "http"
72
+ https = "https"
73
+ ws = "ws"
74
+ wss = "wss"
75
+
76
+
77
+ class ExternalDocumentation(BaseModel):
78
+ description: Optional[str] = None
79
+ url: AnyUrl
80
+
81
+ class Config:
82
+ extra = "allow"
83
+
84
+
85
+ class Reference(BaseModel):
86
+ ref: str = Field(alias="$ref")
87
+
88
+
89
+ class XML(BaseModel):
90
+ name: Optional[str] = None
91
+ namespace: Optional[str] = None
92
+ prefix: Optional[str] = None
93
+ attribute: Optional[bool] = None
94
+ wrapped: Optional[bool] = None
95
+
96
+ class Config:
97
+ extra = "allow"
98
+
99
+
100
+ class Schema(BaseModel):
101
+ ref: Optional[str] = Field(default=None, alias="$ref")
102
+ format: Optional[str] = None
103
+ title: Optional[str] = None
104
+ description: Optional[str] = None
105
+
106
+ default: Optional[Any] = None
107
+ multipleOf: Optional[float] = None
108
+ maximum: Optional[float] = None
109
+ exclusiveMaximum: Optional[bool] = None
110
+ minimum: Optional[float] = None
111
+ exclusiveMinimum: Optional[bool] = None
112
+ maxLength: Optional[int] = Field(default=None, ge=0)
113
+ minLength: Optional[int] = Field(default=None, ge=0)
114
+ pattern: Optional[str] = None
115
+ maxItems: Optional[int] = Field(default=None, ge=0)
116
+ minItems: Optional[int] = Field(default=None, ge=0)
117
+ uniqueItems: Optional[bool] = None
118
+ maxProperties: Optional[int] = Field(default=None, ge=0)
119
+ minProperties: Optional[int] = Field(default=None, ge=0)
120
+ required: Optional[List[str]] = None
121
+ enum: Optional[List[Any]] = None
122
+ type_: Optional[str] = Field(default=None, alias="type")
123
+
124
+ items: Optional[Union["Schema", List["Schema"]]] = None
125
+ allOf: Optional[List["Schema"]] = None
126
+ properties: Optional[Dict[str, "Schema"]] = None
127
+ additionalProperties: Optional[Union["Schema", Reference, bool]] = None
128
+
129
+ discriminator: Optional[str] = None
130
+ readOnly: Optional[bool] = None
131
+ xml: Optional[XML] = None
132
+ externalDocs: Optional[ExternalDocumentation] = None
133
+ example: Optional[Any] = None
134
+
135
+ class Config:
136
+ extra: str = "allow"
137
+
138
+
139
+ class _Schema2(BaseModel):
140
+ type: Optional[str] = None
141
+ format: Optional[str] = None
142
+ items: Optional[Union["Schema", List["Schema"]]] = None
143
+ collectionFormat: Optional[str] = None
144
+ default: Optional[Any] = None
145
+ maximum: Optional[float] = None
146
+ exclusiveMaximum: Optional[bool] = None
147
+ minimum: Optional[float] = None
148
+ exclusiveMinimum: Optional[bool] = None
149
+ maxLength: Optional[int] = Field(default=None, ge=0)
150
+ minLength: Optional[int] = Field(default=None, ge=0)
151
+ pattern: Optional[str] = None
152
+ maxItems: Optional[int] = Field(default=None, ge=0)
153
+ minItems: Optional[int] = Field(default=None, ge=0)
154
+ uniqueItems: Optional[bool] = None
155
+ enum: Optional[List[Any]] = None
156
+ multipleOf: Optional[float] = None
157
+
158
+ class Config:
159
+ extra: str = "allow"
160
+
161
+
162
+ class ParameterSchema(_Schema2):
163
+ allowEmptyValue: bool = False
164
+
165
+ class Config:
166
+ extra: str = "allow"
167
+
168
+
169
+ class ParameterInType(Enum):
170
+ query = "query"
171
+ header = "header"
172
+ path = "path"
173
+ formData = "formData"
174
+ body = "body"
175
+
176
+
177
+ class ParameterBase(BaseModel):
178
+ name: str
179
+ in_: ParameterInType = Field(alias="in")
180
+ description: Optional[str] = None
181
+ required: Optional[bool] = None
182
+
183
+ class Config:
184
+ extra = "allow"
185
+
186
+
187
+ class ParameterBody(ParameterBase):
188
+ schema_: Optional[Union[Schema, ParameterSchema]] = Field(
189
+ default=None, alias="schema"
190
+ )
191
+
192
+
193
+ class ParameterNotBody(ParameterBase, ParameterSchema):
194
+ pass
195
+
196
+ class Config:
197
+ extra = "allow"
198
+
199
+
200
+ class Header(_Schema2):
201
+ pass
202
+
203
+
204
+ class Response(BaseModel):
205
+ description: str
206
+ schema_: Optional[Schema] = Field(default=None, alias="schema")
207
+ headers: Optional[Dict[str, Union[Header, Reference]]] = None
208
+ examples: Optional[Any] = None # XXX
209
+
210
+ class Config:
211
+ extra = "allow"
212
+
213
+
214
+ class Operation(BaseModel):
215
+ tags: Optional[List[str]] = None
216
+ summary: Optional[str] = None
217
+ description: Optional[str] = None
218
+ externalDocs: Optional[ExternalDocumentation] = None
219
+ operationId: Optional[str] = None
220
+ consumes: Optional[List[str]] = None # XXX
221
+ produces: Optional[List[str]] = None # XXX
222
+ parameters: Optional[List[Union[ParameterBody, ParameterNotBody]]] = None
223
+ # Using Any for Specification Extensions
224
+ responses: Dict[str, Union[Response, Any]]
225
+ schemes: Optional[List[URLSchemeEnum]] = None
226
+ deprecated: Optional[bool] = None
227
+ security: Optional[List[Dict[str, List[str]]]] = None
228
+
229
+ class Config:
230
+ extra = "allow"
231
+
232
+
233
+ class PathItem(BaseModel):
234
+ ref: Optional[str] = Field(default=None, alias="$ref")
235
+ get: Optional[Operation] = None
236
+ put: Optional[Operation] = None
237
+ post: Optional[Operation] = None
238
+ delete: Optional[Operation] = None
239
+ options: Optional[Operation] = None
240
+ head: Optional[Operation] = None
241
+ patch: Optional[Operation] = None
242
+ parameters: Optional[List[Union[ParameterBody, ParameterNotBody]]] = None
243
+
244
+ class Config:
245
+ extra = "allow"
246
+
247
+
248
+ class SecuritySchemeType(Enum):
249
+ apiKey = "apiKey"
250
+ basic = "basic"
251
+ oauth2 = "oauth2"
252
+
253
+
254
+ class SecurityBase(BaseModel):
255
+ type_: SecuritySchemeType = Field(alias="type")
256
+ description: Optional[str] = None
257
+
258
+ class Config:
259
+ extra = "allow"
260
+
261
+
262
+ class BasicAuth(SecurityBase):
263
+ pass
264
+
265
+
266
+ class APIKeyIn(Enum):
267
+ query = "query"
268
+ header = "header"
269
+
270
+
271
+ class APIKey(SecurityBase):
272
+ type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type")
273
+ in_: APIKeyIn = Field(alias="in")
274
+ name: str
275
+
276
+
277
+ class OAuth2FlowIn(Enum):
278
+ implicit = "implicit"
279
+ password = "password"
280
+ application = "application"
281
+ accessCode = "accessCode"
282
+
283
+
284
+ class OAuth2FlowBase(SecurityBase):
285
+ flow: OAuth2FlowIn
286
+ scopes: Dict[str, str] = {}
287
+
288
+ class Config:
289
+ extra = "allow"
290
+
291
+
292
+ class OAuth2Implicit(OAuth2FlowBase):
293
+ authorizationUrl: str
294
+
295
+
296
+ class OAuth2Password(OAuth2FlowBase):
297
+ tokenUrl: str
298
+
299
+
300
+ class OAuth2Application(OAuth2FlowBase):
301
+ tokenUrl: str
302
+
303
+
304
+ class OAuth2AccessCode(OAuth2FlowBase):
305
+ authorizationUrl: str
306
+ tokenUrl: str
307
+
308
+
309
+ SecurityScheme = Union[
310
+ BasicAuth,
311
+ APIKey,
312
+ OAuth2Implicit,
313
+ OAuth2Password,
314
+ OAuth2Application,
315
+ OAuth2AccessCode,
316
+ ]
317
+
318
+
319
+ class Tag(BaseModel):
320
+ name: str
321
+ description: Optional[str] = None
322
+ externalDocs: Optional[ExternalDocumentation] = None
323
+
324
+ class Config:
325
+ extra = "allow"
326
+
327
+
328
+ class Swagger2(BaseModel):
329
+ swagger: str
330
+ info: Info
331
+ host: Optional[str] = None # URLHost
332
+ basePath: Optional[str] = None # URLBasePath
333
+ schemes: Optional[List[URLSchemeEnum]] = None
334
+ consumes: Optional[List[str]] = None
335
+ produces: Optional[List[str]] = None
336
+ paths: Dict[str, Union[PathItem, Any]]
337
+ definitions: Optional[Dict[str, Union[Schema, Reference]]] = None
338
+ parameters: Optional[
339
+ Dict[str, Union[ParameterBody, ParameterNotBody, Reference]]
340
+ ] = None
341
+ responses: Optional[Dict[str, Union[Response, Reference]]] = None
342
+ securityDefinitions: Optional[Dict[str, Union[SecurityScheme, Reference]]]
343
+ security: Optional[List[Dict[str, List[str]]]] = None
344
+ tags: Optional[List[Tag]] = None
345
+ externalDocs: Optional[ExternalDocumentation] = None
346
+
347
+ class Config:
348
+ extra = "allow"
349
+
350
+
351
+ Schema.update_forward_refs()
352
+ Operation.update_forward_refs()
353
+ # Encoding.update_forward_refs()
@@ -0,0 +1,456 @@
1
+ import http.client
2
+ import inspect
3
+ from enum import Enum
4
+ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
5
+ from urllib.parse import ParseResult, urlparse
6
+
7
+ from fastapi import routing
8
+ from fastapi.datastructures import DefaultPlaceholder
9
+ from fastapi.dependencies.models import Dependant
10
+ from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
11
+ from fastapi.encoders import jsonable_encoder
12
+ from fastapi.logger import logger
13
+ from fastapi.openapi.constants import METHODS_WITH_BODY
14
+ from fastapi.openapi.utils import (
15
+ get_flat_models_from_routes,
16
+ get_openapi_operation_metadata,
17
+ status_code_ranges,
18
+ )
19
+ from fastapi.params import Body, Param
20
+ from fastapi.responses import Response
21
+ from fastapi.utils import deep_dict_update, is_body_allowed_for_status_code
22
+ from pydantic import BaseModel
23
+ from pydantic.fields import ModelField, Undefined
24
+ from pydantic.schema import field_schema, get_model_name_map, model_process_schema
25
+ from pydantic.utils import lenient_issubclass
26
+ from starlette.responses import JSONResponse
27
+ from starlette.routing import BaseRoute
28
+ from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
29
+
30
+ from fastapi_swagger2.constants import REF_PREFIX
31
+ from fastapi_swagger2.models import Swagger2
32
+
33
+ validation_error_definition = {
34
+ "title": "ValidationError",
35
+ "type": "object",
36
+ "properties": {
37
+ "loc": {
38
+ "title": "Location",
39
+ "type": "array",
40
+ "items": {"type": "string"},
41
+ },
42
+ "msg": {"title": "Message", "type": "string"},
43
+ "type": {"title": "Error Type", "type": "string"},
44
+ },
45
+ "required": ["loc", "msg", "type"],
46
+ }
47
+
48
+ validation_error_response_definition = {
49
+ "title": "HTTPValidationError",
50
+ "type": "object",
51
+ "properties": {
52
+ "detail": {
53
+ "title": "Detail",
54
+ "type": "array",
55
+ "items": {"$ref": REF_PREFIX + "ValidationError"},
56
+ }
57
+ },
58
+ }
59
+
60
+
61
+ def get_model_definitions(
62
+ *,
63
+ flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
64
+ model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
65
+ ) -> Dict[str, Any]:
66
+ definitions: Dict[str, Dict[str, Any]] = {}
67
+ for model in flat_models:
68
+ m_schema, m_definitions, m_nested_models = model_process_schema(
69
+ model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
70
+ )
71
+ definitions.update(m_definitions)
72
+ model_name = model_name_map[model]
73
+ if "description" in m_schema:
74
+ m_schema["description"] = m_schema["description"].split("\f")[0]
75
+ definitions[model_name] = m_schema
76
+ return definitions
77
+
78
+
79
+ def get_swagger2_security_definitions(
80
+ flat_dependant: Dependant,
81
+ ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
82
+ def _map_oauth2_flow(flow_key: str, flow: Dict[str, Any]) -> Dict[str, Any]:
83
+ security_definition = {
84
+ "type": "oauth2",
85
+ "flow": flow_key,
86
+ "scopes": flow["scopes"],
87
+ }
88
+ if "authorizationUrl" in flow:
89
+ security_definition.update({"authorizationUrl": flow["authorizationUrl"]})
90
+ if "tokenUrl" in flow:
91
+ security_definition.update({"tokenUrl": flow["tokenUrl"]})
92
+
93
+ return security_definition
94
+
95
+ oauth2_flows_keys_map = {
96
+ "implicit": "implicit",
97
+ "password": "password",
98
+ "clientCredentials": "application",
99
+ "authorizationCode": "accessCode",
100
+ }
101
+ security_definitions = {}
102
+ operation_security = []
103
+ for security_requirement in flat_dependant.security_requirements:
104
+ # fastapi.security.* which gets model from fastapi.openapi.models
105
+ security_definition = jsonable_encoder(
106
+ security_requirement.security_scheme.model,
107
+ by_alias=True,
108
+ exclude_none=True,
109
+ )
110
+ if security_definition["type"] == "http":
111
+ if security_definition.get("scheme", "basic") == "basic":
112
+ security_definition = {"type": "basic"}
113
+ else:
114
+ logger.warning(
115
+ f"fastapi_swagger2: Unable to handle security_definition: {security_definition}"
116
+ )
117
+ elif security_definition["type"] == "apiKey":
118
+ pass
119
+ elif security_definition["type"] == "oauth2":
120
+ _security_definition = security_definition
121
+ flows = security_definition["flows"]
122
+ flows_keys = list(flows.keys())
123
+ if len(flows_keys) >= 1:
124
+ flow_key_3 = flows_keys[0]
125
+ flow = flows[flow_key_3]
126
+ security_definition = _map_oauth2_flow(
127
+ oauth2_flows_keys_map[flow_key_3], flow
128
+ )
129
+
130
+ for i in range(1, len(flows_keys)):
131
+ flow_key_3 = flows_keys[i]
132
+ flow = flows[flow_key_3]
133
+ flow_key_2 = oauth2_flows_keys_map[flow_key_3]
134
+ security_definition_1 = _map_oauth2_flow(flow_key_2, flow)
135
+ security_name = security_requirement.security_scheme.scheme_name
136
+ _security_name = security_name + "_" + flow_key_2
137
+ security_definitions[_security_name] = security_definition_1
138
+ operation_security.append(
139
+ {_security_name: security_requirement.scopes}
140
+ )
141
+ else:
142
+ logger.warning(
143
+ f"fastapi_swagger2: Unable to handle security_definition: {security_definition}"
144
+ )
145
+ security_name = security_requirement.security_scheme.scheme_name
146
+ security_definitions[security_name] = security_definition
147
+ operation_security.append({security_name: security_requirement.scopes})
148
+ return security_definitions, operation_security
149
+
150
+
151
+ def get_swagger2_operation_parameters(
152
+ *,
153
+ all_route_params: Sequence[ModelField],
154
+ model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
155
+ ) -> List[Dict[str, Any]]:
156
+ parameters = []
157
+ for param in all_route_params:
158
+ field_info = param.field_info
159
+ field_info = cast(Param, field_info)
160
+ if not field_info.include_in_schema:
161
+ continue
162
+ parameter: Dict[str, Any] = {
163
+ "name": param.alias,
164
+ "in": field_info.in_.value,
165
+ "required": param.required,
166
+ }
167
+ schema: Dict[str, Any] = field_schema(
168
+ param, model_name_map=model_name_map, ref_prefix=REF_PREFIX
169
+ )[0]
170
+ if field_info.in_.value == "body":
171
+ parameter["schema"] = schema
172
+ else:
173
+ parameter.update({k: v for (k, v) in schema.items() if k != "title"})
174
+ if field_info.description:
175
+ parameter["description"] = field_info.description
176
+ if field_info.examples:
177
+ parameter["examples"] = jsonable_encoder(field_info.examples)
178
+ elif field_info.example != Undefined:
179
+ parameter["example"] = jsonable_encoder(field_info.example)
180
+ if field_info.deprecated:
181
+ parameter["deprecated"] = field_info.deprecated
182
+ parameters.append(parameter)
183
+ return parameters
184
+
185
+
186
+ def get_swagger2_operation_request_body(
187
+ *,
188
+ body_field: Optional[ModelField],
189
+ model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
190
+ ) -> Optional[Dict[str, Any]]:
191
+ if not body_field:
192
+ return None
193
+ assert isinstance(body_field, ModelField)
194
+ body_schema, _, _ = field_schema(
195
+ body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
196
+ )
197
+ field_info = cast(Body, body_field.field_info)
198
+ # request_media_type = field_info.media_type
199
+ required = body_field.required
200
+ request_body_oai: Dict[str, Any] = {}
201
+ request_body_oai["name"] = body_field.alias
202
+ request_body_oai["in"] = "body"
203
+ if required:
204
+ request_body_oai["required"] = required
205
+
206
+ request_media_content: Dict[str, Any] = {"schema": body_schema}
207
+ if field_info.examples:
208
+ request_media_content["examples"] = jsonable_encoder(field_info.examples)
209
+ elif field_info.example != Undefined:
210
+ request_media_content["example"] = jsonable_encoder(field_info.example)
211
+ # request_body_oai["content"] = {request_media_type: request_media_content}
212
+ request_body_oai.update(request_media_content)
213
+ return request_body_oai
214
+
215
+
216
+ def get_swagger2_path(
217
+ *, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
218
+ ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
219
+ path: Dict[str, Any] = {}
220
+ security_schemes: Dict[str, Any] = {}
221
+ definitions: Dict[str, Any] = {}
222
+ assert route.methods is not None, "Methods must be a list"
223
+
224
+ if isinstance(route.response_class, DefaultPlaceholder):
225
+ current_response_class: Type[Response] = route.response_class.value
226
+ else:
227
+ current_response_class = route.response_class
228
+ assert current_response_class, "A response class is needed to generate Swagger"
229
+
230
+ route_response_media_type: Optional[str] = current_response_class.media_type
231
+ if route.include_in_schema:
232
+ for method in route.methods:
233
+ operation = get_openapi_operation_metadata(
234
+ route=route, method=method, operation_ids=operation_ids
235
+ )
236
+
237
+ parameters: List[Dict[str, Any]] = []
238
+ flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
239
+ (
240
+ security_definitions,
241
+ operation_security,
242
+ ) = get_swagger2_security_definitions(flat_dependant=flat_dependant)
243
+
244
+ if operation_security:
245
+ operation.setdefault("security", []).extend(operation_security)
246
+
247
+ if security_definitions:
248
+ security_schemes.update(security_definitions)
249
+
250
+ all_route_params = get_flat_params(route.dependant)
251
+ operation_parameters = get_swagger2_operation_parameters(
252
+ all_route_params=all_route_params, model_name_map=model_name_map
253
+ )
254
+ parameters.extend(operation_parameters)
255
+ if parameters:
256
+ all_parameters = {
257
+ (param["in"], param["name"]): param for param in parameters
258
+ }
259
+ required_parameters = {
260
+ (param["in"], param["name"]): param
261
+ for param in parameters
262
+ if param.get("required")
263
+ }
264
+ # Make sure required definitions of the same parameter take precedence
265
+ # over non-required definitions
266
+ all_parameters.update(required_parameters)
267
+
268
+ if method in METHODS_WITH_BODY:
269
+ request_body_oai = get_swagger2_operation_request_body(
270
+ body_field=route.body_field, model_name_map=model_name_map
271
+ )
272
+ if request_body_oai:
273
+ all_parameters.update({("body", "body"): request_body_oai})
274
+
275
+ operation["parameters"] = list(all_parameters.values())
276
+
277
+ if route.callbacks:
278
+ callbacks = {}
279
+ for callback in route.callbacks:
280
+ if isinstance(callback, routing.APIRoute):
281
+ (
282
+ cb_path,
283
+ cb_security_schemes,
284
+ cb_definitions,
285
+ ) = get_swagger2_path(
286
+ route=callback,
287
+ model_name_map=model_name_map,
288
+ operation_ids=operation_ids,
289
+ )
290
+ callbacks[callback.name] = {callback.path: cb_path}
291
+ operation["callbacks"] = callbacks
292
+
293
+ if route.status_code is not None:
294
+ status_code = str(route.status_code)
295
+ else:
296
+ # It would probably make more sense for all response classes to have an
297
+ # explicit default status_code, and to extract it from them, instead of
298
+ # doing this inspection tricks, that would probably be in the future
299
+ # TODO: probably make status_code a default class attribute for all
300
+ # responses in Starlette
301
+ response_signature = inspect.signature(current_response_class.__init__)
302
+ status_code_param = response_signature.parameters.get("status_code")
303
+ if status_code_param is not None:
304
+ if isinstance(status_code_param.default, int):
305
+ status_code = str(status_code_param.default)
306
+ operation.setdefault("responses", {}).setdefault(status_code, {})[
307
+ "description"
308
+ ] = route.response_description
309
+
310
+ if route_response_media_type and is_body_allowed_for_status_code(
311
+ route.status_code
312
+ ):
313
+ response_schema = {"type": "string"}
314
+ if lenient_issubclass(current_response_class, JSONResponse):
315
+ if route.response_field:
316
+ response_schema, _, _ = field_schema(
317
+ route.response_field,
318
+ model_name_map=model_name_map,
319
+ ref_prefix=REF_PREFIX,
320
+ )
321
+ else:
322
+ response_schema = {}
323
+ operation.setdefault("responses", {}).setdefault(status_code, {})[
324
+ "schema"
325
+ ] = response_schema
326
+ operation.setdefault("produces", []).append(route_response_media_type)
327
+
328
+ if route.responses:
329
+ operation_responses = operation.setdefault("responses", {})
330
+ for (
331
+ additional_status_code,
332
+ additional_response,
333
+ ) in route.responses.items():
334
+ process_response = additional_response.copy()
335
+ process_response.pop("model", None)
336
+ status_code_key = str(additional_status_code).upper()
337
+ if status_code_key == "DEFAULT":
338
+ status_code_key = "default"
339
+ openapi_response = operation_responses.setdefault(
340
+ status_code_key, {}
341
+ )
342
+ assert isinstance(
343
+ process_response, dict
344
+ ), "An additional response must be a dict"
345
+ field = route.response_fields.get(additional_status_code)
346
+ additional_field_schema: Optional[Dict[str, Any]] = None
347
+ if field:
348
+ additional_field_schema, _, _ = field_schema(
349
+ field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
350
+ )
351
+ # media_type = route_response_media_type or "application/json"
352
+ additional_schema = process_response.setdefault("schema", {})
353
+ deep_dict_update(additional_schema, additional_field_schema)
354
+ status_text: Optional[str] = status_code_ranges.get(
355
+ str(additional_status_code).upper()
356
+ ) or http.client.responses.get(int(additional_status_code))
357
+ description = (
358
+ process_response.get("description")
359
+ or openapi_response.get("description")
360
+ or status_text
361
+ or "Additional Response"
362
+ )
363
+ deep_dict_update(openapi_response, process_response)
364
+ openapi_response["description"] = description
365
+
366
+ http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
367
+ if (all_route_params or route.body_field) and not any(
368
+ status in operation["responses"]
369
+ for status in [http422, "4XX", "default"]
370
+ ):
371
+ operation["responses"][http422] = {
372
+ "description": "Validation Error",
373
+ "schema": {"$ref": REF_PREFIX + "HTTPValidationError"},
374
+ }
375
+ if "ValidationError" not in definitions:
376
+ definitions.update(
377
+ {
378
+ "ValidationError": validation_error_definition,
379
+ "HTTPValidationError": validation_error_response_definition,
380
+ }
381
+ )
382
+ if route.openapi_extra:
383
+ deep_dict_update(operation, route.openapi_extra)
384
+ path[method.lower()] = operation
385
+
386
+ return path, security_schemes, definitions
387
+
388
+
389
+ def get_swagger2(
390
+ *,
391
+ title: str,
392
+ version: str,
393
+ swagger2_version: str = "2.0",
394
+ description: Optional[str] = None,
395
+ routes: Sequence[BaseRoute],
396
+ tags: Optional[List[Dict[str, Any]]] = None,
397
+ server: Optional[str] = None,
398
+ terms_of_service: Optional[str] = None,
399
+ contact: Optional[Dict[str, Union[str, Any]]] = None,
400
+ license_info: Optional[Dict[str, Union[str, Any]]] = None,
401
+ ) -> Dict[str, Any]:
402
+ info: Dict[str, Any] = {"title": title, "version": version}
403
+ if description:
404
+ info["description"] = description
405
+ if terms_of_service:
406
+ info["termsOfService"] = terms_of_service
407
+ if contact:
408
+ info["contact"] = contact
409
+ if license_info:
410
+ info["license"] = license_info
411
+
412
+ output: Dict[str, Any] = {"swagger": swagger2_version, "info": info}
413
+
414
+ if server:
415
+ pr: ParseResult = urlparse(server)
416
+ output["host"] = pr.netloc
417
+ output["basePath"] = pr.path or "/"
418
+ output.setdefault("schemes", []).append(pr.scheme)
419
+
420
+ paths: Dict[str, Dict[str, Any]] = {}
421
+ operation_ids: Set[str] = set()
422
+
423
+ flat_models = get_flat_models_from_routes(routes)
424
+ model_name_map = get_model_name_map(flat_models)
425
+ definitions = get_model_definitions(
426
+ flat_models=flat_models, model_name_map=model_name_map
427
+ )
428
+
429
+ for route in routes:
430
+ if isinstance(route, routing.APIRoute):
431
+ result = get_swagger2_path(
432
+ route=route, model_name_map=model_name_map, operation_ids=operation_ids
433
+ )
434
+ if result:
435
+ path, security_schemes, path_definitions = result
436
+
437
+ if path:
438
+ paths.setdefault(route.path_format, {}).update(path)
439
+
440
+ if security_schemes:
441
+ output.setdefault("securityDefinitions", {}).update(
442
+ security_schemes
443
+ )
444
+
445
+ if path_definitions:
446
+ definitions.update(path_definitions)
447
+
448
+ output["paths"] = paths
449
+
450
+ if definitions:
451
+ output["definitions"] = {k: definitions[k] for k in sorted(definitions)}
452
+
453
+ if tags:
454
+ output["tags"] = tags
455
+
456
+ return jsonable_encoder(Swagger2(**output), by_alias=True, exclude_none=True) # type: ignore
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.1
2
+ Name: fastapi_swagger2
3
+ Version: 0.0.1
4
+ Summary: Swagger2 support for FastAPI framework
5
+ Project-URL: Homepage, https://github.com/virajkanwade/fastapi_swagger2
6
+ Project-URL: Documentation, https://github.com/virajkanwade/fastapi_swagger2
7
+ Author-email: Viraj Kanwade <virajk.oib@gmail.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2023 Viraj Kanwade
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Classifier: Environment :: Web Environment
31
+ Classifier: Framework :: AsyncIO
32
+ Classifier: Framework :: FastAPI
33
+ Classifier: Framework :: Pydantic
34
+ Classifier: Framework :: Pydantic :: 1
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Intended Audience :: Information Technology
37
+ Classifier: Intended Audience :: System Administrators
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Programming Language :: Python
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Classifier: Programming Language :: Python :: 3.8
44
+ Classifier: Programming Language :: Python :: 3.9
45
+ Classifier: Programming Language :: Python :: 3.10
46
+ Classifier: Programming Language :: Python :: 3.11
47
+ Classifier: Topic :: Internet
48
+ Classifier: Topic :: Internet :: WWW/HTTP
49
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
50
+ Classifier: Topic :: Software Development
51
+ Classifier: Topic :: Software Development :: Libraries
52
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
53
+ Classifier: Typing :: Typed
54
+ Requires-Python: >=3.8
55
+ Requires-Dist: fastapi>=0.79.0
56
+ Provides-Extra: all
57
+ Requires-Dist: httpx>=0.23.0; extra == 'all'
58
+ Provides-Extra: dev
59
+ Requires-Dist: ruff==0.0.272; extra == 'dev'
60
+ Provides-Extra: test
61
+ Requires-Dist: black==23.3.0; extra == 'test'
62
+ Requires-Dist: coverage[toml]<8.0,>=6.5.0; extra == 'test'
63
+ Requires-Dist: httpx<0.24.0,>=0.23.0; extra == 'test'
64
+ Requires-Dist: isort<6.0.0,>=5.0.6; extra == 'test'
65
+ Requires-Dist: mypy==1.3.0; extra == 'test'
66
+ Requires-Dist: pytest<8.0.0,>=7.1.3; extra == 'test'
67
+ Requires-Dist: ruff==0.0.272; extra == 'test'
68
+ Description-Content-Type: text/markdown
69
+
70
+ # fastapi_swagger2
71
+ <p align="center">
72
+ Swagger2 support for FastAPI
73
+ </p>
74
+ <p align="center">
75
+ <a href="https://pypi.org/project/fastapi_swagger2" target="_blank">
76
+ <img src="https://img.shields.io/pypi/v/fastapi_swagger2?color=%2334D058&label=pypi%20package" alt="Package version">
77
+ </a>
78
+ <a href="https://pypi.org/project/fastapi_swagger2" target="_blank">
79
+ <img src="https://img.shields.io/pypi/pyversions/fastapi_swagger2.svg?color=%2334D058" alt="Supported Python versions">
80
+ </a>
81
+ </p>
82
+
83
+ ---
84
+
85
+ _Reason behind this library:_
86
+
87
+ Few API GW services like Google Cloud API GW still support only Swagger 2.0 spec. Since FastAPI only supports OAS3, it is a challenge. Converting from OAS3 to Swagger 2.0 requires some manual steps which would hinder CI/CD.
88
+
89
+ ---
90
+
91
+ ## Requirements
92
+
93
+ Python 3.8+
94
+
95
+ FastAPI 0.79.0+
96
+
97
+ ## Installation
98
+
99
+ <div class="termy">
100
+
101
+ ```console
102
+ $ pip install fastapi_swagger2
103
+ ```
104
+
105
+ </div>
106
+
107
+ ## Example
108
+
109
+ ```Python
110
+ from typing import Union
111
+
112
+ from fastapi import FastAPI
113
+ from fastapi_swagger2 import FastAPISwagger2
114
+
115
+ app = FastAPI()
116
+ FastAPISwagger2(app)
117
+
118
+
119
+ @app.get("/")
120
+ def read_root():
121
+ return {"Hello": "World"}
122
+
123
+
124
+ @app.get("/items/{item_id}")
125
+ def read_item(item_id: int, q: Union[str, None] = None):
126
+ return {"item_id": item_id, "q": q}
127
+ ```
128
+
129
+ This adds following endpoints:
130
+ * http://localhost:8000/swagger2.json
131
+ * http://localhost:8000/swagger2/docs
132
+ * http://localhost:8000/swagger2/redoc
133
+
134
+ ## Development
135
+
136
+ ```console
137
+ $ pip install "/path/to/fastapi_swagger2/repo[test,all]"
138
+ ```
@@ -0,0 +1,8 @@
1
+ fastapi_swagger2/__init__.py,sha256=lMqJDEL0ul7-HQkDP7kZt2oT3xxo9obO3OEXI85Klm8,5674
2
+ fastapi_swagger2/constants.py,sha256=CocDvqrd9i-APawDkQMJlSltfg329f5FsFJ0UM2k0wc,30
3
+ fastapi_swagger2/models.py,sha256=Qe_MgqzTuBj2oXHMqdhyCIGFG4QkC4Sz6qLugd2B7Ko,9198
4
+ fastapi_swagger2/utils.py,sha256=ouYny39RgjJ6ddY43-83TLmCjOI9_qc4AJPcAehcDdI,18893
5
+ fastapi_swagger2-0.0.1.dist-info/METADATA,sha256=A2jB_6kfHFf7bK_BWFcG277sdF-W9MiaFh5DsGpORys,4768
6
+ fastapi_swagger2-0.0.1.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
7
+ fastapi_swagger2-0.0.1.dist-info/licenses/LICENSE,sha256=Ro8vX7VuT9CkpLBBRrKO7G2NY0CD5ZVLwL1_snxmd-E,1070
8
+ fastapi_swagger2-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.17.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Viraj Kanwade
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.