diff --git a/README.md b/README.md index 0dd6635..43c9359 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,12 @@ Options: -o, --output PATH [required] -t, --template-dir PATH -m, --model-file Specify generated model file path + name, if not default to models.py + -r, --generate-routers Generate modular api with multiple routers using RouterAPI (for bigger applications). + --specify-tags Use along with --generate-routers to generate specific routers from given list of tags. -c, --custom-visitors PATH - A custom visitor that adds variables to the template. --install-completion Install completion for the current shell. --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. ``` @@ -313,6 +314,79 @@ def {{operation.function_name}}({{operation.snake_case_arguments}}) -> {{operati ``` +### modular template +`modular_template/main.jinja2`: +```jinja +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"} +``` + +`modular_template/routers.jinja2`: +```jinja +from __future__ import annotations + +from fastapi import APIRouter +from fastapi import FastAPI + +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 %} +``` + +`modular_template/dependencies.jinja2`: +```jinja +{{imports}} +``` + ## Custom Visitors Custom visitors allow you to pass custom variables to your custom templates. @@ -346,6 +420,424 @@ def custom_visitor(parser: OpenAPIParser, model_path: Path) -> Dict[str, object] visit: Visitor = custom_visitor ``` +### Multiple Files using APIRouter (For Bigger Applications) + +``` +├── app # "app" is a Root directory +│ ├── main.py # "main" module +│ ├── models.py # "models" of the application +│ ├── dependencies.py # "dependencies" module, e.g. import app.dependencies +│ └── routers # "routers" is a "app subpackage" +│ ├── fat_cats.py # "fat_cats" submodule, e.g. import app.routers.fat_cats +│ ├── slim_dogs.py # "slim_dogs" submodule, e.g. import app.routers.slim_dogs +│ └── wild_boars.py # "wild_boars" submodule, e.g. import app.routers.wild_boars +``` + +See [documentation](https://fastapi.tiangolo.com/tutorial/bigger-applications/) of APIRouter OpenAPI for more details. + +**_Generate main aside with all of its routers_**: +```bash +$ fastapi-codegen --input swagger.yaml --output app --generate-routers +``` + +**_Regenerate specific routers_**: +```bash +$ fastapi-codegen --input swagger.yaml --output app --generate-routers --specify-tags "Wild Boars, Fat Cats" +``` + + +
+swagger.yaml +
+
+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"
+
+
+
+ +`app/main.py`: + +```python +# generated by fastapi-codegen: +# filename: swagger.yaml +# timestamp: 2023-04-04T12:06:16+00:00 + +from __future__ import annotations + +from fastapi import FastAPI + +from .routers import fat_cats, slim_dogs, wild_boars + +app = FastAPI( + version='1.0.0', + title='Swagger Petstore', + license={'name': 'MIT'}, + servers=[ + {'url': '/'}, + {'url': 'http://petstore.swagger.io/v1'}, + {'url': 'http://localhost:8080/'}, + ], +) + +app.include_router(fat_cats.router) +app.include_router(slim_dogs.router) +app.include_router(wild_boars.router) + + +@app.get("/") +async def root(): + return {"message": "Gateway of the App"} +``` + +`app/models.py`: + +```python +# generated by fastapi-codegen: +# filename: swagger.yaml +# timestamp: 2023-04-04T12:06:16+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class Pet(BaseModel): + id: int + name: str + tag: Optional[str] = None + + +class FatCats(BaseModel): + __root__: List[Pet] = Field(..., description='list of fat cats') + + +class SlimDogs(BaseModel): + __root__: List[Pet] = Field(..., description='list of slim dogs') + + +class WildBoars(BaseModel): + __root__: List[Pet] = Field(..., description='list of wild boars') +``` + +`app/routers/fat_cats.py`: + +```python +# generated by fastapi-codegen: +# filename: swagger.yaml +# timestamp: 2023-04-04T12:06:16+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Fat Cats']) + + +@router.get('/cats', response_model=FatCats, tags=['Fat Cats']) +def list_fat_cats(limit: Optional[int] = None) -> FatCats: + """ + List All Fat Cats + """ + pass + + +@router.post('/cats', response_model=None, tags=['Fat Cats']) +def create_fat_cats() -> None: + """ + Create a Fat Cat + """ + pass + + +@router.get('/cats/{cat_id}', response_model=Pet, tags=['Fat Cats']) +def show_cat_by_id(cat_id: str = Path(..., alias='catId')) -> Pet: + """ + Info For a Specific Cat + """ + pass +``` + +`app/routers/slim_dogs.py`: + +```python +# generated by fastapi-codegen: +# filename: swagger.yaml +# timestamp: 2023-04-04T12:06:16+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Slim Dogs']) + + +@router.get('/dogs', response_model=SlimDogs, tags=['Slim Dogs']) +def list_slim_dogs(limit: Optional[int] = None) -> SlimDogs: + """ + List All Slim Dogs + """ + pass + + +@router.post('/dogs', response_model=None, tags=['Slim Dogs']) +def create_slim_dogs() -> None: + """ + Create a Slim Dog + """ + pass + + +@router.get('/dogs/{dog_id}', response_model=Pet, tags=['Slim Dogs']) +def show_dog_by_id(dog_id: str = Path(..., alias='dogId')) -> Pet: + """ + Info For a Specific Dog + """ + pass +``` + +`app/routers/wild_boars.py`: + +```python +# generated by fastapi-codegen: +# filename: swagger.yaml +# timestamp: 2023-04-04T12:06:16+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Wild Boars']) + + +@router.get('/boars', response_model=WildBoars, tags=['Wild Boars']) +def list_wild_boars(limit: Optional[int] = None) -> WildBoars: + """ + List All Wild Boars + """ + pass + + +@router.post('/boars', response_model=None, tags=['Wild Boars']) +def create_wild_boars() -> None: + """ + Create a Wild Boar + """ + pass + + +@router.get('/boars/{boar_id}', response_model=Pet, tags=['Wild Boars']) +def show_boar_by_id(boar_id: str = Path(..., alias='boarId')) -> Pet: + """ + Info For a Specific Boar + """ + pass +``` + +`app/dependencies.py`: + +```python +# generated by fastapi-codegen: +# filename: swagger.yaml +# timestamp: 2023-04-04T12:06:16+00:00 + +from __future__ import annotations + +from typing import Optional + +from fastapi import Path + +from .models import FatCats, Pet, SlimDogs, WildBoars +``` ## PyPi [https://pypi.org/project/fastapi-code-generator](https://pypi.org/project/fastapi-code-generator) diff --git a/fastapi_code_generator/__main__.py b/fastapi_code_generator/__main__.py index 25bb43e..9f214e8 100644 --- a/fastapi_code_generator/__main__.py +++ b/fastapi_code_generator/__main__.py @@ -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 @@ -16,6 +17,12 @@ app = typer.Typer() +all_tags = [] + +TITLE_PATTERN = re.compile(r'(? 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: @@ -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: diff --git a/fastapi_code_generator/modular_template/dependencies.jinja2 b/fastapi_code_generator/modular_template/dependencies.jinja2 new file mode 100644 index 0000000..ef3149f --- /dev/null +++ b/fastapi_code_generator/modular_template/dependencies.jinja2 @@ -0,0 +1 @@ +{{imports}} \ No newline at end of file diff --git a/fastapi_code_generator/modular_template/main.jinja2 b/fastapi_code_generator/modular_template/main.jinja2 new file mode 100644 index 0000000..8d6a33d --- /dev/null +++ b/fastapi_code_generator/modular_template/main.jinja2 @@ -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"} diff --git a/fastapi_code_generator/modular_template/routers.jinja2 b/fastapi_code_generator/modular_template/routers.jinja2 new file mode 100644 index 0000000..0cec624 --- /dev/null +++ b/fastapi_code_generator/modular_template/routers.jinja2 @@ -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 %} \ No newline at end of file diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 0000000..8b12cb7 --- /dev/null +++ b/swagger.yaml @@ -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" \ No newline at end of file diff --git a/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/dependencies.py b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/dependencies.py new file mode 100644 index 0000000..ede6084 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/dependencies.py @@ -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 diff --git a/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/main.py b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/main.py new file mode 100644 index 0000000..b6de4f4 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/main.py @@ -0,0 +1,29 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import FastAPI + +from .routers import fat_cats, slim_dogs, wild_boars + +app = FastAPI( + version='1.0.0', + title='Swagger Petstore', + license={'name': 'MIT'}, + servers=[ + {'url': '/'}, + {'url': 'http://petstore.swagger.io/v1'}, + {'url': 'http://localhost:8080/'}, + ], +) + +app.include_router(fat_cats.router) +app.include_router(slim_dogs.router) +app.include_router(wild_boars.router) + + +@app.get("/") +async def root(): + return {"message": "Gateway of the App"} diff --git a/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/models.py b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/models.py new file mode 100644 index 0000000..ceb4515 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/models.py @@ -0,0 +1,27 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class Pet(BaseModel): + id: int + name: str + tag: Optional[str] = None + + +class FatCats(BaseModel): + __root__: List[Pet] = Field(..., description='list of fat cats') + + +class SlimDogs(BaseModel): + __root__: List[Pet] = Field(..., description='list of slim dogs') + + +class WildBoars(BaseModel): + __root__: List[Pet] = Field(..., description='list of wild boars') diff --git a/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/fat_cats.py b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/fat_cats.py new file mode 100644 index 0000000..77a7da4 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/fat_cats.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Fat Cats']) + + +@router.get('/cats', response_model=FatCats, tags=['Fat Cats']) +def list_fat_cats(limit: Optional[int] = None) -> FatCats: + """ + List All Fat Cats + """ + pass + + +@router.post('/cats', response_model=None, tags=['Fat Cats']) +def create_fat_cats() -> None: + """ + Create a Fat Cat + """ + pass + + +@router.get('/cats/{cat_id}', response_model=Pet, tags=['Fat Cats']) +def show_cat_by_id(cat_id: str = Path(..., alias='catId')) -> Pet: + """ + Info For a Specific Cat + """ + pass diff --git a/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/slim_dogs.py b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/slim_dogs.py new file mode 100644 index 0000000..170237f --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/slim_dogs.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Slim Dogs']) + + +@router.get('/dogs', response_model=SlimDogs, tags=['Slim Dogs']) +def list_slim_dogs(limit: Optional[int] = None) -> SlimDogs: + """ + List All Slim Dogs + """ + return SlimDogs.parse_obj([{"id": "9", "name": "Pluto"}]) + + +@router.post('/dogs', response_model=None, tags=['Slim Dogs']) +def create_slim_dogs() -> None: + """ + Create a Slim Dog + """ + return None + + +@router.get('/dogs/{dog_id}', response_model=Pet, tags=['Slim Dogs']) +def show_dog_by_id(dog_id: str = Path(..., alias='dogId')) -> Pet: + """ + Info For a Specific Dog + """ + return Pet.parse_obj({"id": dog_id, "name": "Pluto"}) \ No newline at end of file diff --git a/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/wild_boars.py b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/wild_boars.py new file mode 100644 index 0000000..b0a849e --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/expected/using_routers_example/routers/wild_boars.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Wild Boars']) + + +@router.get('/boars', response_model=WildBoars, tags=['Wild Boars']) +def list_wild_boars(limit: Optional[int] = None) -> WildBoars: + """ + List All Wild Boars + """ + pass + + +@router.post('/boars', response_model=None, tags=['Wild Boars']) +def create_wild_boars() -> None: + """ + Create a Wild Boar + """ + pass + + +@router.get('/boars/{boar_id}', response_model=Pet, tags=['Wild Boars']) +def show_boar_by_id(boar_id: str = Path(..., alias='boarId')) -> Pet: + """ + Info For a Specific Boar + """ + pass diff --git a/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/dependencies.py b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/dependencies.py new file mode 100644 index 0000000..ede6084 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/dependencies.py @@ -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 diff --git a/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/main.py b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/main.py new file mode 100644 index 0000000..b6de4f4 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/main.py @@ -0,0 +1,29 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import FastAPI + +from .routers import fat_cats, slim_dogs, wild_boars + +app = FastAPI( + version='1.0.0', + title='Swagger Petstore', + license={'name': 'MIT'}, + servers=[ + {'url': '/'}, + {'url': 'http://petstore.swagger.io/v1'}, + {'url': 'http://localhost:8080/'}, + ], +) + +app.include_router(fat_cats.router) +app.include_router(slim_dogs.router) +app.include_router(wild_boars.router) + + +@app.get("/") +async def root(): + return {"message": "Gateway of the App"} diff --git a/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/models.py b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/models.py new file mode 100644 index 0000000..ceb4515 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/models.py @@ -0,0 +1,27 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class Pet(BaseModel): + id: int + name: str + tag: Optional[str] = None + + +class FatCats(BaseModel): + __root__: List[Pet] = Field(..., description='list of fat cats') + + +class SlimDogs(BaseModel): + __root__: List[Pet] = Field(..., description='list of slim dogs') + + +class WildBoars(BaseModel): + __root__: List[Pet] = Field(..., description='list of wild boars') diff --git a/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/fat_cats.py b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/fat_cats.py new file mode 100644 index 0000000..77a7da4 --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/fat_cats.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Fat Cats']) + + +@router.get('/cats', response_model=FatCats, tags=['Fat Cats']) +def list_fat_cats(limit: Optional[int] = None) -> FatCats: + """ + List All Fat Cats + """ + pass + + +@router.post('/cats', response_model=None, tags=['Fat Cats']) +def create_fat_cats() -> None: + """ + Create a Fat Cat + """ + pass + + +@router.get('/cats/{cat_id}', response_model=Pet, tags=['Fat Cats']) +def show_cat_by_id(cat_id: str = Path(..., alias='catId')) -> Pet: + """ + Info For a Specific Cat + """ + pass diff --git a/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/slim_dogs.py b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/slim_dogs.py new file mode 100644 index 0000000..170237f --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/slim_dogs.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Slim Dogs']) + + +@router.get('/dogs', response_model=SlimDogs, tags=['Slim Dogs']) +def list_slim_dogs(limit: Optional[int] = None) -> SlimDogs: + """ + List All Slim Dogs + """ + return SlimDogs.parse_obj([{"id": "9", "name": "Pluto"}]) + + +@router.post('/dogs', response_model=None, tags=['Slim Dogs']) +def create_slim_dogs() -> None: + """ + Create a Slim Dog + """ + return None + + +@router.get('/dogs/{dog_id}', response_model=Pet, tags=['Slim Dogs']) +def show_dog_by_id(dog_id: str = Path(..., alias='dogId')) -> Pet: + """ + Info For a Specific Dog + """ + return Pet.parse_obj({"id": dog_id, "name": "Pluto"}) \ No newline at end of file diff --git a/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/wild_boars.py b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/wild_boars.py new file mode 100644 index 0000000..b0a849e --- /dev/null +++ b/tests/data/expected/openapi/modify_specific_routers/modified/using_routers_example/routers/wild_boars.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Wild Boars']) + + +@router.get('/boars', response_model=WildBoars, tags=['Wild Boars']) +def list_wild_boars(limit: Optional[int] = None) -> WildBoars: + """ + List All Wild Boars + """ + pass + + +@router.post('/boars', response_model=None, tags=['Wild Boars']) +def create_wild_boars() -> None: + """ + Create a Wild Boar + """ + pass + + +@router.get('/boars/{boar_id}', response_model=Pet, tags=['Wild Boars']) +def show_boar_by_id(boar_id: str = Path(..., alias='boarId')) -> Pet: + """ + Info For a Specific Boar + """ + pass diff --git a/tests/data/expected/openapi/using_routers/using_routers_example/dependencies.py b/tests/data/expected/openapi/using_routers/using_routers_example/dependencies.py new file mode 100644 index 0000000..ede6084 --- /dev/null +++ b/tests/data/expected/openapi/using_routers/using_routers_example/dependencies.py @@ -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 diff --git a/tests/data/expected/openapi/using_routers/using_routers_example/main.py b/tests/data/expected/openapi/using_routers/using_routers_example/main.py new file mode 100644 index 0000000..b6de4f4 --- /dev/null +++ b/tests/data/expected/openapi/using_routers/using_routers_example/main.py @@ -0,0 +1,29 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import FastAPI + +from .routers import fat_cats, slim_dogs, wild_boars + +app = FastAPI( + version='1.0.0', + title='Swagger Petstore', + license={'name': 'MIT'}, + servers=[ + {'url': '/'}, + {'url': 'http://petstore.swagger.io/v1'}, + {'url': 'http://localhost:8080/'}, + ], +) + +app.include_router(fat_cats.router) +app.include_router(slim_dogs.router) +app.include_router(wild_boars.router) + + +@app.get("/") +async def root(): + return {"message": "Gateway of the App"} diff --git a/tests/data/expected/openapi/using_routers/using_routers_example/models.py b/tests/data/expected/openapi/using_routers/using_routers_example/models.py new file mode 100644 index 0000000..ceb4515 --- /dev/null +++ b/tests/data/expected/openapi/using_routers/using_routers_example/models.py @@ -0,0 +1,27 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class Pet(BaseModel): + id: int + name: str + tag: Optional[str] = None + + +class FatCats(BaseModel): + __root__: List[Pet] = Field(..., description='list of fat cats') + + +class SlimDogs(BaseModel): + __root__: List[Pet] = Field(..., description='list of slim dogs') + + +class WildBoars(BaseModel): + __root__: List[Pet] = Field(..., description='list of wild boars') diff --git a/tests/data/expected/openapi/using_routers/using_routers_example/routers/fat_cats.py b/tests/data/expected/openapi/using_routers/using_routers_example/routers/fat_cats.py new file mode 100644 index 0000000..77a7da4 --- /dev/null +++ b/tests/data/expected/openapi/using_routers/using_routers_example/routers/fat_cats.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Fat Cats']) + + +@router.get('/cats', response_model=FatCats, tags=['Fat Cats']) +def list_fat_cats(limit: Optional[int] = None) -> FatCats: + """ + List All Fat Cats + """ + pass + + +@router.post('/cats', response_model=None, tags=['Fat Cats']) +def create_fat_cats() -> None: + """ + Create a Fat Cat + """ + pass + + +@router.get('/cats/{cat_id}', response_model=Pet, tags=['Fat Cats']) +def show_cat_by_id(cat_id: str = Path(..., alias='catId')) -> Pet: + """ + Info For a Specific Cat + """ + pass diff --git a/tests/data/expected/openapi/using_routers/using_routers_example/routers/slim_dogs.py b/tests/data/expected/openapi/using_routers/using_routers_example/routers/slim_dogs.py new file mode 100644 index 0000000..776c622 --- /dev/null +++ b/tests/data/expected/openapi/using_routers/using_routers_example/routers/slim_dogs.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Slim Dogs']) + + +@router.get('/dogs', response_model=SlimDogs, tags=['Slim Dogs']) +def list_slim_dogs(limit: Optional[int] = None) -> SlimDogs: + """ + List All Slim Dogs + """ + pass + + +@router.post('/dogs', response_model=None, tags=['Slim Dogs']) +def create_slim_dogs() -> None: + """ + Create a Slim Dog + """ + pass + + +@router.get('/dogs/{dog_id}', response_model=Pet, tags=['Slim Dogs']) +def show_dog_by_id(dog_id: str = Path(..., alias='dogId')) -> Pet: + """ + Info For a Specific Dog + """ + pass diff --git a/tests/data/expected/openapi/using_routers/using_routers_example/routers/wild_boars.py b/tests/data/expected/openapi/using_routers/using_routers_example/routers/wild_boars.py new file mode 100644 index 0000000..b0a849e --- /dev/null +++ b/tests/data/expected/openapi/using_routers/using_routers_example/routers/wild_boars.py @@ -0,0 +1,35 @@ +# generated by fastapi-codegen: +# filename: using_routers_example.yaml +# timestamp: 2023-04-11T00:00:00+00:00 + +from __future__ import annotations + +from fastapi import APIRouter + +from ..dependencies import * + +router = APIRouter(tags=['Wild Boars']) + + +@router.get('/boars', response_model=WildBoars, tags=['Wild Boars']) +def list_wild_boars(limit: Optional[int] = None) -> WildBoars: + """ + List All Wild Boars + """ + pass + + +@router.post('/boars', response_model=None, tags=['Wild Boars']) +def create_wild_boars() -> None: + """ + Create a Wild Boar + """ + pass + + +@router.get('/boars/{boar_id}', response_model=Pet, tags=['Wild Boars']) +def show_boar_by_id(boar_id: str = Path(..., alias='boarId')) -> Pet: + """ + Info For a Specific Boar + """ + pass diff --git a/tests/data/modular_template/dependencies.jinja2 b/tests/data/modular_template/dependencies.jinja2 new file mode 100644 index 0000000..ef3149f --- /dev/null +++ b/tests/data/modular_template/dependencies.jinja2 @@ -0,0 +1 @@ +{{imports}} \ No newline at end of file diff --git a/tests/data/modular_template/main.jinja2 b/tests/data/modular_template/main.jinja2 new file mode 100644 index 0000000..8d6a33d --- /dev/null +++ b/tests/data/modular_template/main.jinja2 @@ -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"} diff --git a/tests/data/modular_template/routers.jinja2 b/tests/data/modular_template/routers.jinja2 new file mode 100644 index 0000000..0cec624 --- /dev/null +++ b/tests/data/modular_template/routers.jinja2 @@ -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 %} \ No newline at end of file diff --git a/tests/data/openapi/using_routers/using_routers_example.yaml b/tests/data/openapi/using_routers/using_routers_example.yaml new file mode 100644 index 0000000..8b12cb7 --- /dev/null +++ b/tests/data/openapi/using_routers/using_routers_example.yaml @@ -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" \ No newline at end of file diff --git a/tests/test_generate.py b/tests/test_generate.py index 507d74b..095cf1c 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -11,11 +11,16 @@ OPEN_API_SECURITY_TEMPLATE_DIR_NAME = Path('openapi') / 'custom_template_security' OPEN_API_REMOTE_REF_DIR_NAME = Path('openapi') / 'remote_ref' OPEN_API_DISABLE_TIMESTAMP_DIR_NAME = Path('openapi') / 'disable_timestamp' +OPEN_API_USING_ROUTERS_DIR_NAME = Path('openapi') / 'using_routers' DATA_DIR = Path(__file__).parent / 'data' EXPECTED_DIR = DATA_DIR / 'expected' +BUILTIN_MODULAR_TEMPLATE_DIR = DATA_DIR / 'modular_template' + +SPECIFIC_TAGS = 'Wild Boars, Fat Cats' + @pytest.mark.parametrize( "oas_file", (DATA_DIR / OPEN_API_DEFAULT_TEMPLATE_DIR_NAME).glob("*.yaml") @@ -111,3 +116,64 @@ def test_disable_timestamp(oas_file): assert [f.name for f in output_files] == [f.name for f in expected_files] for output_file, expected_file in zip(output_files, expected_files): assert output_file.read_text() == expected_file.read_text() + + +@pytest.mark.parametrize( + "oas_file", (DATA_DIR / OPEN_API_USING_ROUTERS_DIR_NAME).glob("*.yaml") +) +@freeze_time("2023-04-11") +def test_generate_using_routers(oas_file): + with TemporaryDirectory() as tmp_dir: + output_dir = Path(tmp_dir) / oas_file.stem + Path(output_dir / "routers").mkdir(parents=True, exist_ok=True) + generate_code( + input_name=oas_file.name, + input_text=oas_file.read_text(), + output_dir=output_dir, + template_dir=BUILTIN_MODULAR_TEMPLATE_DIR, + generate_routers=True, + ) + expected_dir = EXPECTED_DIR / OPEN_API_USING_ROUTERS_DIR_NAME / oas_file.stem + output_files = sorted(list(output_dir.glob('*'))) + expected_files = sorted(list(expected_dir.glob('*'))) + assert [f.name for f in output_files] == [f.name for f in expected_files] + for output_file, expected_file in zip(output_files, expected_files): + if output_file.is_dir() and expected_file.is_dir(): + output_inners = sorted(list((output_dir / output_file).glob('*'))) + expected_inners = sorted(list((expected_dir / expected_file).glob('*'))) + for output_inner, expected_inner in zip(output_inners, expected_inners): + assert output_inner.read_text() == expected_inner.read_text() + else: + assert output_file.read_text() == expected_file.read_text() + + +@pytest.mark.parametrize( + "oas_file", (DATA_DIR / OPEN_API_USING_ROUTERS_DIR_NAME).glob("*.yaml") +) +@freeze_time("2023-04-11") +def test_generate_modify_specific_routers(oas_file): + test_dir = EXPECTED_DIR / 'openapi/modify_specific_routers/modified' + output_dir = Path(test_dir) / oas_file.stem + Path(output_dir / "routers").mkdir(parents=True, exist_ok=True) + generate_code( + input_name=oas_file.name, + input_text=oas_file.read_text(), + output_dir=output_dir, + template_dir=BUILTIN_MODULAR_TEMPLATE_DIR, + generate_routers=True, + specify_tags=SPECIFIC_TAGS, + ) + expected_dir = ( + EXPECTED_DIR / 'openapi/modify_specific_routers/expected' / oas_file.stem + ) + output_files = sorted(list(output_dir.glob('*'))) + expected_files = sorted(list(expected_dir.glob('*'))) + assert [f.name for f in output_files] == [f.name for f in expected_files] + for output_file, expected_file in zip(output_files, expected_files): + if output_file.is_dir() and expected_file.is_dir(): + output_inners = sorted(list((output_dir / output_file).glob('*'))) + expected_inners = sorted(list((expected_dir / expected_file).glob('*'))) + for output_inner, expected_inner in zip(output_inners, expected_inners): + assert output_inner.read_text() == expected_inner.read_text() + else: + assert output_file.read_text() == expected_file.read_text()