Skip to content

Commit 11508ea

Browse files
authored
Merge pull request #22 from koxudaxi/support_security
Support security
2 parents 74a8176 + 253e7b2 commit 11508ea

File tree

14 files changed

+410
-15
lines changed

14 files changed

+410
-15
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ You can use below variables in jinja2 template
262262
- `operation.response` response object
263263
- `operation.function_name` function name is created `operationId` or `METHOD` + `Path`
264264
- `operation.snake_case_arguments` Snake-cased function arguments
265+
- `operation.security` [Security](https://swagger.io/docs/specification/authentication/)
265266

266267
### default template
267268
`main.jinja2`

fastapi_code_generator/parser.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class Operation(CachedPropertyModel):
8989
responses: Dict[UsefulStr, Any] = {}
9090
requestBody: Dict[str, Any] = {}
9191
imports: List[Import] = []
92+
security: Optional[List[Dict[str, List[str]]]] = None
9293

9394
@cached_property
9495
def root_path(self) -> UsefulStr:
@@ -299,6 +300,7 @@ class Operations(BaseModel):
299300
options: Optional[Operation] = None
300301
trace: Optional[Operation] = None
301302
path: UsefulStr
303+
security: Optional[List[Dict[str, List[str]]]] = []
302304

303305
@root_validator(pre=True)
304306
def inject_path_and_type_to_operation(cls, values: Dict[str, Any]) -> Any:
@@ -311,30 +313,40 @@ def inject_path_and_type_to_operation(cls, values: Dict[str, Any]) -> Any:
311313
},
312314
path=path,
313315
parameters=values.get('parameters', []),
316+
security=values.get('security'),
314317
)
315318

316319
@root_validator
317-
def inject_parameters_to_operation(cls, values: Dict[str, Any]) -> Any:
318-
if parameters := values.get('parameters'):
319-
for operation_name in OPERATION_NAMES:
320-
if operation := values.get(operation_name):
320+
def inject_parameters_and_security_to_operation(cls, values: Dict[str, Any]) -> Any:
321+
security = values.get('security')
322+
for operation_name in OPERATION_NAMES:
323+
if operation := values.get(operation_name):
324+
if parameters := values.get('parameters'):
321325
operation.parameters.extend(parameters)
326+
if security is not None and operation.security is None:
327+
operation.security = security
328+
322329
return values
323330

324331

325332
class Path(CachedPropertyModel):
326333
path: UsefulStr
327334
operations: Optional[Operations] = None
335+
security: Optional[List[Dict[str, List[str]]]] = []
328336

329337
@root_validator(pre=True)
330338
def validate_root(cls, values: Dict[str, Any]) -> Any:
331339
if path := values.get('path'):
332340
if isinstance(path, str):
333341
if operations := values.get('operations'):
334342
if isinstance(operations, dict):
343+
security = values.get('security', [])
335344
return {
336345
'path': path,
337-
'operations': dict(**operations, path=path),
346+
'operations': dict(
347+
**operations, path=path, security=security
348+
),
349+
'security': security,
338350
}
339351
return values
340352

@@ -379,15 +391,21 @@ def __init__(
379391

380392
def parse(self) -> ParsedObject:
381393
openapi = load_json_or_yaml(self.input_text)
382-
return self.parse_paths(openapi["paths"])
394+
return self.parse_paths(openapi)
395+
396+
def parse_security(
397+
self, openapi: Dict[str, Any]
398+
) -> Optional[List[Dict[str, List[str]]]]:
399+
return openapi.get('security')
383400

384-
def parse_paths(self, paths: Dict[str, Any]) -> ParsedObject:
401+
def parse_paths(self, openapi: Dict[str, Any]) -> ParsedObject:
402+
security = self.parse_security(openapi)
385403
return ParsedObject(
386404
[
387405
operation
388-
for path_name, operations in paths.items()
406+
for path_name, operations in openapi['paths'].items()
389407
for operation in Path(
390-
path=UsefulStr(path_name), operations=operations
408+
path=UsefulStr(path_name), operations=operations, security=security
391409
).exists_operations
392410
]
393411
)

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ freezegun = "^0.3.15"
5353
line-length = 88
5454
skip-string-normalization = true
5555
target-version = ['py38']
56+
exclude = '(tests/data|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|.*\/models\.py.*|.*\/models\/.*)'
5657

5758
[tool.isort]
5859
multi_line_output = 3
5960
include_trailing_comma = true
6061
force_grid_wrap = 0
6162
use_parentheses = true
6263
line_length = 88
64+
skip = "tests/data"
6365

6466
[tool.pydantic-pycharm-plugin.parsable-types]
6567
# str field may parse int and float
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
from __future__ import annotations
3+
4+
from typing import List, Optional
5+
6+
from fastapi import Depends, FastAPI, HTTPException, Query
7+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
8+
from pydantic import BaseModel
9+
from starlette import status
10+
11+
{{ imports }}
12+
13+
app = FastAPI()
14+
15+
16+
DUMMY_CREDENTIALS = 'abcdefg'
17+
18+
19+
class User(BaseModel):
20+
username: str
21+
email: str
22+
23+
24+
def get_dummy_user(token: str) -> User:
25+
return User(username=token, email='[email protected]')
26+
27+
28+
async def valid_token(auth: HTTPAuthorizationCredentials = Depends(HTTPBearer())) -> str:
29+
if auth.credentials == DUMMY_CREDENTIALS:
30+
return 'dummy'
31+
raise HTTPException(
32+
status_code=status.HTTP_401_UNAUTHORIZED,
33+
detail="Invalid authentication credentials",
34+
headers={"WWW-Authenticate": "Bearer"},
35+
)
36+
37+
38+
async def valid_current_user(token: str = Depends(valid_token)) -> User:
39+
return get_dummy_user(token)
40+
41+
42+
43+
{% for operation in operations %}
44+
@app.{{operation.type}}('{{operation.snake_case_path}}', response_model={{operation.response}})
45+
def {{operation.function_name}}({{operation.snake_case_arguments}}{%- if operation.security -%}{%- if operation.snake_case_arguments -%}, {%- endif -%}user: User = Depends(valid_current_user){%- endif -%}) -> {{operation.response}}:
46+
pass
47+
{% endfor %}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# generated by fastapi-codegen:
2+
# filename: custom_security.yaml
3+
# timestamp: 2020-06-19T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
from pydantic import BaseModel
10+
11+
from fastapi import Depends, FastAPI, HTTPException, Query
12+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
13+
from starlette import status
14+
15+
from .models import Pet, PetForm
16+
17+
app = FastAPI()
18+
19+
20+
DUMMY_CREDENTIALS = 'abcdefg'
21+
22+
23+
class User(BaseModel):
24+
username: str
25+
email: str
26+
27+
28+
def get_dummy_user(token: str) -> User:
29+
return User(username=token, email='[email protected]')
30+
31+
32+
async def valid_token(
33+
auth: HTTPAuthorizationCredentials = Depends(HTTPBearer()),
34+
) -> str:
35+
if auth.credentials == DUMMY_CREDENTIALS:
36+
return 'dummy'
37+
raise HTTPException(
38+
status_code=status.HTTP_401_UNAUTHORIZED,
39+
detail="Invalid authentication credentials",
40+
headers={"WWW-Authenticate": "Bearer"},
41+
)
42+
43+
44+
async def valid_current_user(token: str = Depends(valid_token)) -> User:
45+
return get_dummy_user(token)
46+
47+
48+
@app.get('/food/{food_id}', response_model=None)
49+
def show_food_by_id(food_id: str, user: User = Depends(valid_current_user)) -> None:
50+
pass
51+
52+
53+
@app.get('/pets', response_model=List[Pet])
54+
def list_pets(
55+
limit: Optional[int] = 0,
56+
home_address: Optional[str] = Query('Unknown', alias='HomeAddress'),
57+
kind: Optional[str] = 'dog',
58+
) -> List[Pet]:
59+
pass
60+
61+
62+
@app.post('/pets', response_model=None)
63+
def post_pets(body: PetForm, user: User = Depends(valid_current_user)) -> None:
64+
pass
65+
66+
67+
@app.get('/pets/{pet_id}', response_model=Pet)
68+
def show_pet_by_id(
69+
pet_id: str = Query(..., alias='petId'), user: User = Depends(valid_current_user)
70+
) -> Pet:
71+
pass
72+
73+
74+
@app.put('/pets/{pet_id}', response_model=None)
75+
def put_pets_pet_id(
76+
pet_id: str = Query(..., alias='petId'),
77+
body: PetForm = None,
78+
user: User = Depends(valid_current_user),
79+
) -> None:
80+
pass
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# generated by datamodel-codegen:
2+
# filename: custom_security.yaml
3+
# timestamp: 2020-06-19T00:00:00+00:00
4+
5+
from typing import Optional
6+
7+
from pydantic import BaseModel
8+
9+
10+
class Pet(BaseModel):
11+
id: int
12+
name: str
13+
tag: Optional[str] = None
14+
15+
16+
class Error(BaseModel):
17+
code: int
18+
message: str
19+
20+
21+
class PetForm(BaseModel):
22+
name: Optional[str] = None
23+
age: Optional[int] = None

0 commit comments

Comments
 (0)