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.
- fastapi_swagger2/__init__.py +145 -0
- fastapi_swagger2/constants.py +1 -0
- fastapi_swagger2/models.py +353 -0
- fastapi_swagger2/utils.py +456 -0
- fastapi_swagger2-0.0.1.dist-info/METADATA +138 -0
- fastapi_swagger2-0.0.1.dist-info/RECORD +8 -0
- fastapi_swagger2-0.0.1.dist-info/WHEEL +4 -0
- fastapi_swagger2-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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,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.
|