Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/generate routers #344

Merged
merged 20 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
494 changes: 493 additions & 1 deletion README.md

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions fastapi_code_generator/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from datetime import datetime, timezone
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
Expand All @@ -16,6 +17,12 @@

app = typer.Typer()

all_tags = []

TITLE_PATTERN = re.compile(r'(?<!^)(?<![A-Z ])(?=[A-Z])| ')

BUILTIN_MODULAR_TEMPLATE_DIR = Path(__file__).parent / "modular_template"

BUILTIN_TEMPLATE_DIR = Path(__file__).parent / "template"

BUILTIN_VISITOR_DIR = Path(__file__).parent / "visitors"
Expand Down Expand Up @@ -43,6 +50,8 @@ def main(
enum_field_as_literal: Optional[LiteralType] = typer.Option(
None, "--enum-field-as-literal"
),
generate_routers: bool = typer.Option(False, "--generate-routers", "-r"),
specify_tags: Optional[str] = typer.Option(None, "--specify-tags"),
custom_visitors: Optional[List[Path]] = typer.Option(
None, "--custom-visitor", "-c"
),
Expand All @@ -54,6 +63,7 @@ def main(
model_path = Path(model_file).with_suffix('.py')
else:
model_path = MODEL_PATH

if enum_field_as_literal:
return generate_code(
input_name,
Expand All @@ -63,6 +73,8 @@ def main(
model_path,
enum_field_as_literal,
disable_timestamp=disable_timestamp,
generate_routers=generate_routers,
specify_tags=specify_tags,
)
return generate_code(
input_name,
Expand All @@ -72,6 +84,8 @@ def main(
model_path,
custom_visitors=custom_visitors,
disable_timestamp=disable_timestamp,
generate_routers=generate_routers,
specify_tags=specify_tags,
)


Expand All @@ -94,11 +108,16 @@ def generate_code(
enum_field_as_literal: Optional[str] = None,
custom_visitors: Optional[List[Path]] = [],
disable_timestamp: bool = False,
generate_routers: Optional[bool] = None,
specify_tags: Optional[str] = None,
) -> None:
if not model_path:
model_path = MODEL_PATH
if not output_dir.exists():
output_dir.mkdir(parents=True)
if generate_routers:
template_dir = BUILTIN_MODULAR_TEMPLATE_DIR
Path(output_dir / "routers").mkdir(parents=True, exist_ok=True)
if not template_dir:
template_dir = BUILTIN_TEMPLATE_DIR
if enum_field_as_literal:
Expand Down Expand Up @@ -143,12 +162,52 @@ def generate_code(
visitor_result = visitor(parser, model_path)
template_vars = {**template_vars, **visitor_result}

if generate_routers:
operations: Any = template_vars.get("operations", [])
for operation in operations:
if hasattr(operation, "tags"):
for tag in operation.tags:
all_tags.append(tag)
# Convert from Tag Names to router_names
sorted_tags = sorted(set(all_tags))
routers = sorted(
[re.sub(TITLE_PATTERN, '_', tag.strip()).lower() for tag in sorted_tags]
)
template_vars = {**template_vars, "routers": routers, "tags": sorted_tags}

for target in template_dir.rglob("*"):
relative_path = target.relative_to(template_dir)
template = environment.get_template(str(relative_path))
result = template.render(template_vars)
results[relative_path] = code_formatter.format_code(result)

if generate_routers:
tags = sorted_tags
results.pop(Path("routers.jinja2"))
if specify_tags:
if Path(output_dir.joinpath("main.py")).exists():
with open(Path(output_dir.joinpath("main.py")), 'r') as file:
content = file.read()
if "app.include_router" in content:
tags = sorted(
set(tag.strip() for tag in str(specify_tags).split(","))
)

for target in BUILTIN_MODULAR_TEMPLATE_DIR.rglob("routers.*"):
relative_path = target.relative_to(template_dir)
for router, tag in zip(routers, sorted_tags):
if (
not Path(output_dir.joinpath("routers", router))
.with_suffix(".py")
.exists()
or tag in tags
):
template_vars["tag"] = tag.strip()
template = environment.get_template(str(relative_path))
result = template.render(template_vars)
router_path = Path("routers", router).with_suffix(".jinja2")
results[router_path] = code_formatter.format_code(result)

timestamp = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
header = f"""\
# generated by fastapi-codegen:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{imports}}
22 changes: 22 additions & 0 deletions fastapi_code_generator/modular_template/main.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from fastapi import FastAPI

from .routers import {{ routers | join(", ") }}

app = FastAPI(
{% if info %}
{% for key,value in info.items() %}
{% set info_value= value.__repr__() %}
{{ key }} = {{info_value}},
{% endfor %}
{% endif %}
)

{% for router in routers -%}
app.include_router({{router}}.router)
{% endfor -%}

@app.get("/")
async def root():
return {"message": "Gateway of the App"}
36 changes: 36 additions & 0 deletions fastapi_code_generator/modular_template/routers.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

from fastapi import APIRouter

from ..dependencies import *

router = APIRouter(
tags=['{{tag}}']
)

{% for operation in operations %}
{% if operation.tags[0] == tag %}
@router.{{operation.type}}('{{operation.snake_case_path}}', response_model={{operation.response}}
{% if operation.additional_responses %}
, responses={
{% for status_code, models in operation.additional_responses.items() %}
'{{ status_code }}': {
{% for key, model in models.items() %}
'{{ key }}': {{ model }}{% if not loop.last %},{% endif %}
{% endfor %}
}{% if not loop.last %},{% endif %}
{% endfor %}
}
{% endif %}
{% if operation.tags%}
, tags={{operation.tags}}
{% endif %})
def {{operation.function_name}}({{operation.snake_case_arguments}}) -> {{operation.return_type}}:
{%- if operation.summary %}
"""
{{ operation.summary }}
"""
{%- endif %}
pass
{% endif %}
{% endfor %}
184 changes: 184 additions & 0 deletions swagger.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@

openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: /
- url: http://petstore.swagger.io/v1
- url: http://localhost:8080/
paths:
/boars:
get:
summary: List All Wild Boars
operationId: listWildBoars
tags:
- Wild Boars
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
responses:
'200':
description: An array of wild boars
content:
application/json:
schema:
$ref: "#/components/schemas/WildBoars"
post:
summary: Create a Wild Boar
operationId: createWildBoars
tags:
- Wild Boars
responses:
'201':
description: Null response
/boars/{boarId}:
get:
summary: Info For a Specific Boar
operationId: showBoarById
tags:
- Wild Boars
parameters:
- name: boarId
in: path
required: true
description: The id of the boar to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
/cats:
get:
summary: List All Fat Cats
operationId: listFatCats
tags:
- Fat Cats
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
responses:
'200':
description: An array of fat cats
content:
application/json:
schema:
$ref: "#/components/schemas/FatCats"
post:
summary: Create a Fat Cat
operationId: createFatCats
tags:
- Fat Cats
responses:
'201':
description: Null response
/cats/{catId}:
get:
summary: Info For a Specific Cat
operationId: showCatById
tags:
- Fat Cats
parameters:
- name: catId
in: path
required: true
description: The id of the cat to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
/dogs:
get:
summary: List All Slim Dogs
operationId: listSlimDogs
tags:
- Slim Dogs
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
responses:
'200':
description: An array of slim dogs
content:
application/json:
schema:
$ref: "#/components/schemas/SlimDogs"
post:
summary: Create a Slim Dog
operationId: createSlimDogs
tags:
- Slim Dogs
responses:
'201':
description: Null response
/dogs/{dogId}:
get:
summary: Info For a Specific Dog
operationId: showDogById
tags:
- Slim Dogs
parameters:
- name: dogId
in: path
required: true
description: The id of the dog to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
components:
schemas:
Pet:
required:
- id
- name
properties:
id:
type: integer
name:
type: string
tag:
type: string
FatCats:
type: array
description: list of fat cats
items:
$ref: "#/components/schemas/Pet"
SlimDogs:
type: array
description: list of slim dogs
items:
$ref: "#/components/schemas/Pet"
WildBoars:
type: array
description: list of wild boars
items:
$ref: "#/components/schemas/Pet"
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# generated by fastapi-codegen:
# filename: using_routers_example.yaml
# timestamp: 2023-04-11T00:00:00+00:00

from __future__ import annotations

from typing import Optional

from fastapi import Path

from .models import FatCats, Pet, SlimDogs, WildBoars
Loading