django-cfg 1.4.9__py3-none-any.whl → 1.4.11__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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/middleware/api_access.py +6 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +2 -1
- django_cfg/apps/payments/middleware/usage_tracking.py +5 -1
- django_cfg/apps/payments/models/managers/api_key_managers.py +0 -1
- django_cfg/apps/payments/models/managers/subscription_managers.py +0 -1
- django_cfg/apps/payments/services/core/balance_service.py +5 -5
- django_cfg/apps/payments/services/core/subscription_service.py +1 -2
- django_cfg/apps/payments/views/api/balances.py +8 -7
- django_cfg/apps/payments/views/api/base.py +10 -6
- django_cfg/apps/payments/views/api/currencies.py +53 -10
- django_cfg/apps/payments/views/api/payments.py +3 -1
- django_cfg/apps/payments/views/api/subscriptions.py +2 -5
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +82 -41
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/dashboard/sections/documentation.py +391 -0
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -265
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +238 -0
- django_cfg/models/django/{revolution.py → revolution_legacy.py} +8 -0
- django_cfg/modules/django_admin/management/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +188 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +767 -0
- django_cfg/modules/django_client/core/generator/python.py +751 -0
- django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
- django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
- django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/typescript.py +872 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/spectacular/__init__.py +9 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +16 -5
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/modules/django_unfold/dashboard.py +1 -1
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/RECORD +192 -71
- django_cfg/management/commands/generate.py +0 -107
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,751 @@
|
|
1
|
+
"""
|
2
|
+
Python Generator - Generates Python client (Pydantic 2 + httpx).
|
3
|
+
|
4
|
+
This generator creates a complete Python API client from IR:
|
5
|
+
- Pydantic 2 models (Request/Response/Patch splits)
|
6
|
+
- Enum classes from x-enum-varnames
|
7
|
+
- httpx.AsyncClient for async HTTP
|
8
|
+
- Django CSRF/session handling
|
9
|
+
- Type-safe (MyPy strict mode compatible)
|
10
|
+
|
11
|
+
Reference: https://docs.pydantic.dev/latest/
|
12
|
+
"""
|
13
|
+
|
14
|
+
from __future__ import annotations
|
15
|
+
|
16
|
+
import pathlib
|
17
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
18
|
+
|
19
|
+
from .base import BaseGenerator, GeneratedFile
|
20
|
+
from ..ir import IROperationObject, IRSchemaObject
|
21
|
+
|
22
|
+
|
23
|
+
class PythonGenerator(BaseGenerator):
|
24
|
+
"""
|
25
|
+
Python client generator.
|
26
|
+
|
27
|
+
Generates:
|
28
|
+
- models.py: Pydantic 2 models (User, UserRequest, PatchedUser)
|
29
|
+
- enums.py: Enum classes (StatusEnum, RoleEnum)
|
30
|
+
- client.py: AsyncClient with all operations
|
31
|
+
- __init__.py: Package exports
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(self, *args, **kwargs):
|
35
|
+
super().__init__(*args, **kwargs)
|
36
|
+
|
37
|
+
# Setup Jinja2 environment
|
38
|
+
templates_dir = pathlib.Path(__file__).parent / "templates"
|
39
|
+
self.jinja_env = Environment(
|
40
|
+
loader=FileSystemLoader(str(templates_dir)),
|
41
|
+
autoescape=select_autoescape(['html', 'xml']),
|
42
|
+
trim_blocks=True,
|
43
|
+
lstrip_blocks=True,
|
44
|
+
)
|
45
|
+
|
46
|
+
def generate(self) -> list[GeneratedFile]:
|
47
|
+
"""Generate all Python client files."""
|
48
|
+
files = []
|
49
|
+
|
50
|
+
if self.client_structure == "namespaced":
|
51
|
+
# Generate per-app folders
|
52
|
+
ops_by_tag = self.group_operations_by_tag()
|
53
|
+
|
54
|
+
for tag, operations in sorted(ops_by_tag.items()):
|
55
|
+
# Generate app folder (models.py, client.py, __init__.py)
|
56
|
+
files.extend(self._generate_app_folder(tag, operations))
|
57
|
+
|
58
|
+
# Generate shared enums.py (Variant 2: all enums in root)
|
59
|
+
all_schemas = self.context.schemas
|
60
|
+
all_enums = self._collect_enums_from_schemas(all_schemas)
|
61
|
+
if all_enums:
|
62
|
+
files.append(self._generate_shared_enums_file(all_enums))
|
63
|
+
|
64
|
+
# Generate main client.py
|
65
|
+
files.append(self._generate_main_client_file(ops_by_tag))
|
66
|
+
|
67
|
+
# Generate main __init__.py
|
68
|
+
files.append(self._generate_main_init_file())
|
69
|
+
|
70
|
+
# Generate logger.py with Rich
|
71
|
+
files.append(self._generate_logger_file())
|
72
|
+
|
73
|
+
# Generate schema.py with OpenAPI schema
|
74
|
+
if self.openapi_schema:
|
75
|
+
files.append(self._generate_schema_file())
|
76
|
+
else:
|
77
|
+
# Flat structure (original logic)
|
78
|
+
files.append(self._generate_models_file())
|
79
|
+
|
80
|
+
enum_schemas = self.get_enum_schemas()
|
81
|
+
if enum_schemas:
|
82
|
+
files.append(self._generate_enums_file())
|
83
|
+
|
84
|
+
files.append(self._generate_client_file())
|
85
|
+
files.append(self._generate_init_file())
|
86
|
+
|
87
|
+
# Generate logger.py with Rich
|
88
|
+
files.append(self._generate_logger_file())
|
89
|
+
|
90
|
+
# Generate schema.py with OpenAPI schema
|
91
|
+
if self.openapi_schema:
|
92
|
+
files.append(self._generate_schema_file())
|
93
|
+
|
94
|
+
return files
|
95
|
+
|
96
|
+
# ===== Models Generation =====
|
97
|
+
|
98
|
+
def _generate_models_file(self) -> GeneratedFile:
|
99
|
+
"""Generate models.py with all Pydantic models."""
|
100
|
+
# Generate all schemas
|
101
|
+
schema_codes = []
|
102
|
+
|
103
|
+
# Response models first
|
104
|
+
for name, schema in self.get_response_schemas().items():
|
105
|
+
schema_codes.append(self.generate_schema(schema))
|
106
|
+
|
107
|
+
# Request models
|
108
|
+
for name, schema in self.get_request_schemas().items():
|
109
|
+
schema_codes.append(self.generate_schema(schema))
|
110
|
+
|
111
|
+
# Patch models
|
112
|
+
for name, schema in self.get_patch_schemas().items():
|
113
|
+
schema_codes.append(self.generate_schema(schema))
|
114
|
+
|
115
|
+
template = self.jinja_env.get_template('python/models/models.py.jinja')
|
116
|
+
content = template.render(
|
117
|
+
has_enums=bool(self.get_enum_schemas()),
|
118
|
+
schemas=schema_codes
|
119
|
+
)
|
120
|
+
|
121
|
+
return GeneratedFile(
|
122
|
+
path="models.py",
|
123
|
+
content=content,
|
124
|
+
description="Pydantic 2 models (Request/Response/Patch)",
|
125
|
+
)
|
126
|
+
|
127
|
+
def _generate_enums_file(self) -> GeneratedFile:
|
128
|
+
"""Generate enums.py with all Enum classes (flat structure)."""
|
129
|
+
# Generate all enums
|
130
|
+
enum_codes = []
|
131
|
+
for name, schema in self.get_enum_schemas().items():
|
132
|
+
enum_codes.append(self.generate_enum(schema))
|
133
|
+
|
134
|
+
template = self.jinja_env.get_template('python/models/enums.py.jinja')
|
135
|
+
content = template.render(enums=enum_codes)
|
136
|
+
|
137
|
+
return GeneratedFile(
|
138
|
+
path="enums.py",
|
139
|
+
content=content,
|
140
|
+
description="Enum classes from x-enum-varnames",
|
141
|
+
)
|
142
|
+
|
143
|
+
def _generate_shared_enums_file(self, enums: dict[str, IRSchemaObject]) -> GeneratedFile:
|
144
|
+
"""Generate shared enums.py for namespaced structure (Variant 2)."""
|
145
|
+
# Generate all enums
|
146
|
+
enum_codes = []
|
147
|
+
for name, schema in enums.items():
|
148
|
+
enum_codes.append(self.generate_enum(schema))
|
149
|
+
|
150
|
+
template = self.jinja_env.get_template('python/models/enums.py.jinja')
|
151
|
+
content = template.render(enums=enum_codes)
|
152
|
+
|
153
|
+
return GeneratedFile(
|
154
|
+
path="enums.py",
|
155
|
+
content=content,
|
156
|
+
description="Shared enum classes from x-enum-varnames",
|
157
|
+
)
|
158
|
+
|
159
|
+
# ===== Schema Generation =====
|
160
|
+
|
161
|
+
def generate_schema(self, schema: IRSchemaObject) -> str:
|
162
|
+
"""Generate Pydantic model for schema."""
|
163
|
+
if schema.type != "object":
|
164
|
+
# For primitive types, skip (they'll be inlined)
|
165
|
+
return ""
|
166
|
+
|
167
|
+
# Class docstring
|
168
|
+
docstring_lines = []
|
169
|
+
if schema.description:
|
170
|
+
docstring_lines.extend(self.wrap_comment(schema.description, 76))
|
171
|
+
|
172
|
+
# Add metadata about model type
|
173
|
+
if schema.is_request_model:
|
174
|
+
docstring_lines.append("")
|
175
|
+
docstring_lines.append("Request model (no read-only fields).")
|
176
|
+
elif schema.is_patch_model:
|
177
|
+
docstring_lines.append("")
|
178
|
+
docstring_lines.append("PATCH model (all fields optional).")
|
179
|
+
elif schema.is_response_model:
|
180
|
+
docstring_lines.append("")
|
181
|
+
docstring_lines.append("Response model (includes read-only fields).")
|
182
|
+
|
183
|
+
docstring = "\n".join(docstring_lines) if docstring_lines else None
|
184
|
+
|
185
|
+
# Fields
|
186
|
+
field_lines = []
|
187
|
+
for prop_name, prop_schema in schema.properties.items():
|
188
|
+
field_lines.append(self._generate_field(prop_name, prop_schema, schema.required))
|
189
|
+
|
190
|
+
template = self.jinja_env.get_template('python/models/schema_class.py.jinja')
|
191
|
+
return template.render(
|
192
|
+
name=schema.name,
|
193
|
+
docstring=docstring,
|
194
|
+
fields=field_lines
|
195
|
+
)
|
196
|
+
|
197
|
+
def _generate_field(
|
198
|
+
self,
|
199
|
+
name: str,
|
200
|
+
schema: IRSchemaObject,
|
201
|
+
required_fields: list[str],
|
202
|
+
) -> str:
|
203
|
+
"""
|
204
|
+
Generate Pydantic field definition.
|
205
|
+
|
206
|
+
Examples:
|
207
|
+
id: int
|
208
|
+
username: str
|
209
|
+
email: str | None = None
|
210
|
+
age: int = Field(..., ge=0, le=150)
|
211
|
+
status: StatusEnum
|
212
|
+
"""
|
213
|
+
# Check if this field is an enum
|
214
|
+
if schema.enum and schema.name:
|
215
|
+
# Use enum type from shared enums
|
216
|
+
python_type = schema.name
|
217
|
+
if schema.nullable:
|
218
|
+
python_type = f"{python_type} | None"
|
219
|
+
# Check if this field is a reference to an enum (via $ref)
|
220
|
+
elif schema.ref and schema.ref in self.context.schemas:
|
221
|
+
ref_schema = self.context.schemas[schema.ref]
|
222
|
+
if ref_schema.enum:
|
223
|
+
# This is a reference to an enum component
|
224
|
+
python_type = schema.ref
|
225
|
+
if schema.nullable:
|
226
|
+
python_type = f"{python_type} | None"
|
227
|
+
else:
|
228
|
+
# Regular reference
|
229
|
+
python_type = schema.python_type
|
230
|
+
else:
|
231
|
+
# Get Python type
|
232
|
+
python_type = schema.python_type
|
233
|
+
|
234
|
+
# Check if required
|
235
|
+
is_required = name in required_fields
|
236
|
+
|
237
|
+
# Build Field() kwargs
|
238
|
+
field_kwargs = []
|
239
|
+
|
240
|
+
if schema.description:
|
241
|
+
field_kwargs.append(f"description={schema.description!r}")
|
242
|
+
|
243
|
+
# Validation constraints
|
244
|
+
if schema.min_length is not None:
|
245
|
+
field_kwargs.append(f"min_length={schema.min_length}")
|
246
|
+
if schema.max_length is not None:
|
247
|
+
field_kwargs.append(f"max_length={schema.max_length}")
|
248
|
+
if schema.pattern:
|
249
|
+
field_kwargs.append(f"pattern={schema.pattern!r}")
|
250
|
+
if schema.minimum is not None:
|
251
|
+
field_kwargs.append(f"ge={schema.minimum}")
|
252
|
+
if schema.maximum is not None:
|
253
|
+
field_kwargs.append(f"le={schema.maximum}")
|
254
|
+
|
255
|
+
# Example
|
256
|
+
if schema.example:
|
257
|
+
field_kwargs.append(f"examples=[{schema.example!r}]")
|
258
|
+
|
259
|
+
# Default value
|
260
|
+
if is_required:
|
261
|
+
if field_kwargs:
|
262
|
+
default = f"Field({', '.join(field_kwargs)})"
|
263
|
+
else:
|
264
|
+
default = "..."
|
265
|
+
else:
|
266
|
+
if field_kwargs:
|
267
|
+
default = f"Field(None, {', '.join(field_kwargs)})"
|
268
|
+
else:
|
269
|
+
default = "None"
|
270
|
+
|
271
|
+
return f"{name}: {python_type} = {default}"
|
272
|
+
|
273
|
+
def generate_enum(self, schema: IRSchemaObject) -> str:
|
274
|
+
"""Generate Enum class from x-enum-varnames."""
|
275
|
+
# Determine enum base class
|
276
|
+
if schema.type == "integer":
|
277
|
+
base_class = "IntEnum"
|
278
|
+
else:
|
279
|
+
base_class = "StrEnum"
|
280
|
+
|
281
|
+
# Class docstring
|
282
|
+
docstring_lines = []
|
283
|
+
if schema.description:
|
284
|
+
docstring_lines.extend(self.wrap_comment(schema.description, 76))
|
285
|
+
|
286
|
+
docstring = "\n".join(docstring_lines) if docstring_lines else None
|
287
|
+
|
288
|
+
# Enum members
|
289
|
+
member_lines = []
|
290
|
+
for var_name, value in zip(schema.enum_var_names, schema.enum):
|
291
|
+
if isinstance(value, str):
|
292
|
+
member_lines.append(f'{var_name} = "{value}"')
|
293
|
+
else:
|
294
|
+
member_lines.append(f"{var_name} = {value}")
|
295
|
+
|
296
|
+
template = self.jinja_env.get_template('python/models/enum_class.py.jinja')
|
297
|
+
return template.render(
|
298
|
+
name=schema.name,
|
299
|
+
base_class=base_class,
|
300
|
+
docstring=docstring,
|
301
|
+
members=member_lines
|
302
|
+
)
|
303
|
+
|
304
|
+
# ===== Client Generation =====
|
305
|
+
|
306
|
+
def _generate_client_file(self) -> GeneratedFile:
|
307
|
+
"""Generate client.py with AsyncClient."""
|
308
|
+
# Client class
|
309
|
+
client_code = self._generate_client_class()
|
310
|
+
|
311
|
+
template = self.jinja_env.get_template('python/client_file.py.jinja')
|
312
|
+
content = template.render(
|
313
|
+
has_enums=bool(self.get_enum_schemas()),
|
314
|
+
client_code=client_code
|
315
|
+
)
|
316
|
+
|
317
|
+
return GeneratedFile(
|
318
|
+
path="client.py",
|
319
|
+
content=content,
|
320
|
+
description="AsyncClient with httpx",
|
321
|
+
)
|
322
|
+
|
323
|
+
def _generate_client_class(self) -> str:
|
324
|
+
"""Generate APIClient class."""
|
325
|
+
if self.client_structure == "namespaced":
|
326
|
+
return self._generate_namespaced_client()
|
327
|
+
else:
|
328
|
+
return self._generate_flat_client()
|
329
|
+
|
330
|
+
def _generate_flat_client(self) -> str:
|
331
|
+
"""Generate flat APIClient (all methods in one class)."""
|
332
|
+
# Generate all operation methods
|
333
|
+
method_codes = []
|
334
|
+
for op_id, operation in self.context.operations.items():
|
335
|
+
method_codes.append(self.generate_operation(operation))
|
336
|
+
|
337
|
+
template = self.jinja_env.get_template('python/client/flat_client.py.jinja')
|
338
|
+
return template.render(
|
339
|
+
api_title=self.context.openapi_info.title,
|
340
|
+
operations=method_codes
|
341
|
+
)
|
342
|
+
|
343
|
+
def _generate_namespaced_client(self) -> str:
|
344
|
+
"""Generate namespaced APIClient (sub-clients per tag)."""
|
345
|
+
# Group operations by tag (using base class method)
|
346
|
+
ops_by_tag = self.group_operations_by_tag()
|
347
|
+
|
348
|
+
# Generate sub-client classes
|
349
|
+
sub_client_classes = []
|
350
|
+
for tag, operations in sorted(ops_by_tag.items()):
|
351
|
+
sub_client_classes.append(self._generate_sub_client_class(tag, operations))
|
352
|
+
|
353
|
+
sub_clients_code = "\n\n\n".join(sub_client_classes)
|
354
|
+
|
355
|
+
# Generate main APIClient
|
356
|
+
main_client_code = self._generate_main_client_class(ops_by_tag)
|
357
|
+
|
358
|
+
return f"{sub_clients_code}\n\n\n{main_client_code}"
|
359
|
+
|
360
|
+
def _generate_sub_client_class(self, tag: str, operations: list) -> str:
|
361
|
+
"""Generate sub-client class for a specific tag."""
|
362
|
+
class_name = self.tag_to_class_name(tag)
|
363
|
+
|
364
|
+
# Generate methods for this tag
|
365
|
+
method_codes = []
|
366
|
+
for operation in operations:
|
367
|
+
method_codes.append(self.generate_operation(operation, remove_tag_prefix=True))
|
368
|
+
|
369
|
+
template = self.jinja_env.get_template('python/client/sub_client.py.jinja')
|
370
|
+
return template.render(
|
371
|
+
tag=self.tag_to_display_name(tag),
|
372
|
+
class_name=class_name,
|
373
|
+
operations=method_codes
|
374
|
+
)
|
375
|
+
|
376
|
+
def _generate_main_client_class(self, ops_by_tag: dict) -> str:
|
377
|
+
"""Generate main APIClient with sub-clients."""
|
378
|
+
tags = sorted(ops_by_tag.keys())
|
379
|
+
|
380
|
+
# Prepare tags data for template
|
381
|
+
tags_data = [
|
382
|
+
{
|
383
|
+
"class_name": self.tag_to_class_name(tag),
|
384
|
+
"property": self.tag_to_property_name(tag),
|
385
|
+
}
|
386
|
+
for tag in tags
|
387
|
+
]
|
388
|
+
|
389
|
+
template = self.jinja_env.get_template('python/client/main_client.py.jinja')
|
390
|
+
return template.render(
|
391
|
+
api_title=self.context.openapi_info.title,
|
392
|
+
tags=tags_data
|
393
|
+
)
|
394
|
+
|
395
|
+
def generate_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
|
396
|
+
"""Generate async method for operation."""
|
397
|
+
# Get method name
|
398
|
+
method_name = operation.operation_id
|
399
|
+
if remove_tag_prefix and operation.tags:
|
400
|
+
# Remove tag prefix using base class method
|
401
|
+
tag = operation.tags[0]
|
402
|
+
method_name = self.remove_tag_prefix(method_name, tag)
|
403
|
+
|
404
|
+
# Method signature
|
405
|
+
params = ["self"]
|
406
|
+
|
407
|
+
# Add path parameters
|
408
|
+
for param in operation.path_parameters:
|
409
|
+
param_type = self._map_param_type(param.schema_type)
|
410
|
+
params.append(f"{param.name}: {param_type}")
|
411
|
+
|
412
|
+
# Add request body parameter
|
413
|
+
if operation.request_body:
|
414
|
+
params.append(f"data: {operation.request_body.schema_name}")
|
415
|
+
elif operation.patch_request_body:
|
416
|
+
params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
|
417
|
+
|
418
|
+
# Add query parameters
|
419
|
+
for param in operation.query_parameters:
|
420
|
+
param_type = self._map_param_type(param.schema_type)
|
421
|
+
if not param.required:
|
422
|
+
param_type = f"{param_type} | None = None"
|
423
|
+
params.append(f"{param.name}: {param_type}")
|
424
|
+
|
425
|
+
# Return type
|
426
|
+
primary_response = operation.primary_success_response
|
427
|
+
if primary_response and primary_response.schema_name:
|
428
|
+
if operation.is_list_operation:
|
429
|
+
return_type = f"list[{primary_response.schema_name}]"
|
430
|
+
else:
|
431
|
+
return_type = primary_response.schema_name
|
432
|
+
else:
|
433
|
+
return_type = "None"
|
434
|
+
|
435
|
+
signature = f"async def {method_name}({', '.join(params)}) -> {return_type}:"
|
436
|
+
|
437
|
+
# Docstring
|
438
|
+
docstring_lines = []
|
439
|
+
if operation.summary:
|
440
|
+
docstring_lines.append(operation.summary)
|
441
|
+
if operation.description:
|
442
|
+
if docstring_lines:
|
443
|
+
docstring_lines.append("")
|
444
|
+
docstring_lines.extend(self.wrap_comment(operation.description, 72))
|
445
|
+
|
446
|
+
docstring = "\n".join(docstring_lines) if docstring_lines else None
|
447
|
+
|
448
|
+
# Method body
|
449
|
+
body_lines = []
|
450
|
+
|
451
|
+
# Build URL
|
452
|
+
url_expr = f'"{operation.path}"'
|
453
|
+
if operation.path_parameters:
|
454
|
+
# Replace {id} with f-string {id}
|
455
|
+
url_expr = f'f"{operation.path}"'
|
456
|
+
|
457
|
+
body_lines.append(f"url = {url_expr}")
|
458
|
+
|
459
|
+
# Build request
|
460
|
+
request_kwargs = []
|
461
|
+
|
462
|
+
# Query params
|
463
|
+
if operation.query_parameters:
|
464
|
+
query_items = []
|
465
|
+
for param in operation.query_parameters:
|
466
|
+
if param.required:
|
467
|
+
query_items.append(f'"{param.name}": {param.name}')
|
468
|
+
else:
|
469
|
+
query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
|
470
|
+
|
471
|
+
query_dict = "{" + ", ".join(query_items) + "}"
|
472
|
+
request_kwargs.append(f"params={query_dict}")
|
473
|
+
|
474
|
+
# JSON body
|
475
|
+
if operation.request_body or operation.patch_request_body:
|
476
|
+
request_kwargs.append("json=data.model_dump() if data else None")
|
477
|
+
|
478
|
+
# Make request
|
479
|
+
method_lower = operation.http_method.lower()
|
480
|
+
request_line = f"response = await self._client.{method_lower}(url"
|
481
|
+
if request_kwargs:
|
482
|
+
request_line += ", " + ", ".join(request_kwargs)
|
483
|
+
request_line += ")"
|
484
|
+
|
485
|
+
body_lines.append(request_line)
|
486
|
+
|
487
|
+
# Handle response
|
488
|
+
body_lines.append("response.raise_for_status()")
|
489
|
+
|
490
|
+
if return_type != "None":
|
491
|
+
if operation.is_list_operation:
|
492
|
+
# Paginated list response - extract results
|
493
|
+
body_lines.append(f"data = response.json()")
|
494
|
+
body_lines.append(f'return [{ primary_response.schema_name}.model_validate(item) for item in data.get("results", [])]')
|
495
|
+
else:
|
496
|
+
body_lines.append(f"return {primary_response.schema_name}.model_validate(response.json())")
|
497
|
+
else:
|
498
|
+
body_lines.append("return None")
|
499
|
+
|
500
|
+
template = self.jinja_env.get_template('python/client/operation_method.py.jinja')
|
501
|
+
return template.render(
|
502
|
+
method_name=method_name,
|
503
|
+
params=params,
|
504
|
+
return_type=return_type,
|
505
|
+
docstring=docstring,
|
506
|
+
body_lines=body_lines
|
507
|
+
)
|
508
|
+
|
509
|
+
def _map_param_type(self, schema_type: str) -> str:
|
510
|
+
"""Map parameter schema type to Python type."""
|
511
|
+
type_map = {
|
512
|
+
"string": "str",
|
513
|
+
"integer": "int",
|
514
|
+
"number": "float",
|
515
|
+
"boolean": "bool",
|
516
|
+
"array": "list[Any]",
|
517
|
+
}
|
518
|
+
return type_map.get(schema_type, "Any")
|
519
|
+
|
520
|
+
# ===== Package Init =====
|
521
|
+
|
522
|
+
def _generate_init_file(self) -> GeneratedFile:
|
523
|
+
"""Generate __init__.py with exports."""
|
524
|
+
template = self.jinja_env.get_template('python/__init__.py.jinja')
|
525
|
+
content = template.render(
|
526
|
+
has_enums=bool(self.get_enum_schemas())
|
527
|
+
)
|
528
|
+
|
529
|
+
return GeneratedFile(
|
530
|
+
path="__init__.py",
|
531
|
+
content=content,
|
532
|
+
description="Package exports",
|
533
|
+
)
|
534
|
+
|
535
|
+
# ===== Per-App Folder Generation (Namespaced Structure) =====
|
536
|
+
|
537
|
+
def _generate_app_folder(self, tag: str, operations: list[IROperationObject]) -> list[GeneratedFile]:
|
538
|
+
"""Generate folder for a specific app (tag)."""
|
539
|
+
files = []
|
540
|
+
|
541
|
+
# Get schemas used by this app
|
542
|
+
app_schemas = self._get_schemas_for_operations(operations)
|
543
|
+
|
544
|
+
# Generate models.py for this app
|
545
|
+
files.append(self._generate_app_models_file(tag, app_schemas, operations))
|
546
|
+
|
547
|
+
# Generate client.py for this app
|
548
|
+
files.append(self._generate_app_client_file(tag, operations))
|
549
|
+
|
550
|
+
# Generate __init__.py for this app
|
551
|
+
files.append(self._generate_app_init_file(tag, operations))
|
552
|
+
|
553
|
+
return files
|
554
|
+
|
555
|
+
def _get_schemas_for_operations(self, operations: list[IROperationObject]) -> dict[str, IRSchemaObject]:
|
556
|
+
"""Get all schemas used by given operations."""
|
557
|
+
schemas = {}
|
558
|
+
|
559
|
+
for operation in operations:
|
560
|
+
# Request body schemas
|
561
|
+
if operation.request_body and operation.request_body.schema_name:
|
562
|
+
schema_name = operation.request_body.schema_name
|
563
|
+
if schema_name in self.context.schemas:
|
564
|
+
schemas[schema_name] = self.context.schemas[schema_name]
|
565
|
+
|
566
|
+
# Patch request body schemas
|
567
|
+
if operation.patch_request_body and operation.patch_request_body.schema_name:
|
568
|
+
schema_name = operation.patch_request_body.schema_name
|
569
|
+
if schema_name in self.context.schemas:
|
570
|
+
schemas[schema_name] = self.context.schemas[schema_name]
|
571
|
+
|
572
|
+
# Response schemas
|
573
|
+
for status_code, response in operation.responses.items():
|
574
|
+
if response.schema_name:
|
575
|
+
if response.schema_name in self.context.schemas:
|
576
|
+
schemas[response.schema_name] = self.context.schemas[response.schema_name]
|
577
|
+
|
578
|
+
return schemas
|
579
|
+
|
580
|
+
def _generate_app_models_file(self, tag: str, schemas: dict[str, IRSchemaObject], operations: list[IROperationObject]) -> GeneratedFile:
|
581
|
+
"""Generate models.py for a specific app."""
|
582
|
+
# Check if we have enums in schemas
|
583
|
+
app_enums = self._collect_enums_from_schemas(schemas)
|
584
|
+
has_enums = len(app_enums) > 0
|
585
|
+
|
586
|
+
# Generate schemas
|
587
|
+
schema_codes = []
|
588
|
+
for name, schema in schemas.items():
|
589
|
+
schema_codes.append(self.generate_schema(schema))
|
590
|
+
|
591
|
+
template = self.jinja_env.get_template('python/models/app_models.py.jinja')
|
592
|
+
content = template.render(
|
593
|
+
has_enums=has_enums,
|
594
|
+
enum_names=sorted(app_enums.keys()) if has_enums else [],
|
595
|
+
schemas=schema_codes if schema_codes else ["pass"]
|
596
|
+
)
|
597
|
+
|
598
|
+
folder_name = self.tag_and_app_to_folder_name(tag, operations)
|
599
|
+
return GeneratedFile(
|
600
|
+
path=f"{folder_name}/models.py",
|
601
|
+
content=content,
|
602
|
+
description=f"Pydantic models for {tag}",
|
603
|
+
)
|
604
|
+
|
605
|
+
def _generate_app_client_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
|
606
|
+
"""Generate client.py for a specific app."""
|
607
|
+
class_name = self.tag_to_class_name(tag)
|
608
|
+
|
609
|
+
# Generate methods
|
610
|
+
method_codes = []
|
611
|
+
for operation in operations:
|
612
|
+
method_codes.append(self.generate_operation(operation, remove_tag_prefix=True))
|
613
|
+
|
614
|
+
template = self.jinja_env.get_template('python/client/app_client.py.jinja')
|
615
|
+
content = template.render(
|
616
|
+
tag=self.tag_to_display_name(tag),
|
617
|
+
class_name=class_name,
|
618
|
+
operations=method_codes
|
619
|
+
)
|
620
|
+
|
621
|
+
folder_name = self.tag_and_app_to_folder_name(tag, operations)
|
622
|
+
return GeneratedFile(
|
623
|
+
path=f"{folder_name}/client.py",
|
624
|
+
content=content,
|
625
|
+
description=f"API client for {tag}",
|
626
|
+
)
|
627
|
+
|
628
|
+
def _generate_app_init_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
|
629
|
+
"""Generate __init__.py for a specific app."""
|
630
|
+
class_name = self.tag_to_class_name(tag)
|
631
|
+
|
632
|
+
template = self.jinja_env.get_template('python/app_init.py.jinja')
|
633
|
+
content = template.render(class_name=class_name)
|
634
|
+
|
635
|
+
folder_name = self.tag_and_app_to_folder_name(tag, operations)
|
636
|
+
return GeneratedFile(
|
637
|
+
path=f"{folder_name}/__init__.py",
|
638
|
+
content=content,
|
639
|
+
description=f"Package exports for {tag}",
|
640
|
+
)
|
641
|
+
|
642
|
+
def _generate_main_client_file(self, ops_by_tag: dict) -> GeneratedFile:
|
643
|
+
"""Generate main client.py with APIClient."""
|
644
|
+
tags = sorted(ops_by_tag.keys())
|
645
|
+
|
646
|
+
# Prepare tags data for template
|
647
|
+
tags_data = [
|
648
|
+
{
|
649
|
+
"class_name": self.tag_to_class_name(tag),
|
650
|
+
"slug": self.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
|
651
|
+
}
|
652
|
+
for tag in tags
|
653
|
+
]
|
654
|
+
|
655
|
+
# Generate main APIClient class
|
656
|
+
client_code = self._generate_main_client_class(ops_by_tag)
|
657
|
+
|
658
|
+
template = self.jinja_env.get_template('python/client/main_client_file.py.jinja')
|
659
|
+
content = template.render(
|
660
|
+
tags=tags_data,
|
661
|
+
client_code=client_code
|
662
|
+
)
|
663
|
+
|
664
|
+
return GeneratedFile(
|
665
|
+
path="client.py",
|
666
|
+
content=content,
|
667
|
+
description="Main API client",
|
668
|
+
)
|
669
|
+
|
670
|
+
def _generate_main_init_file(self) -> GeneratedFile:
|
671
|
+
"""Generate main __init__.py with API class and JWT management."""
|
672
|
+
ops_by_tag = self.group_operations_by_tag()
|
673
|
+
tags = sorted(ops_by_tag.keys())
|
674
|
+
|
675
|
+
# Prepare tags data for template
|
676
|
+
tags_data = [
|
677
|
+
{
|
678
|
+
"class_name": self.tag_to_class_name(tag),
|
679
|
+
"slug": self.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
|
680
|
+
}
|
681
|
+
for tag in tags
|
682
|
+
]
|
683
|
+
|
684
|
+
# Check if we have enums
|
685
|
+
all_schemas = self.context.schemas
|
686
|
+
all_enums = self._collect_enums_from_schemas(all_schemas)
|
687
|
+
|
688
|
+
# API class
|
689
|
+
api_class = self._generate_api_wrapper_class_python(tags)
|
690
|
+
|
691
|
+
template = self.jinja_env.get_template('python/main_init.py.jinja')
|
692
|
+
content = template.render(
|
693
|
+
api_title=self.context.openapi_info.title,
|
694
|
+
tags=tags_data,
|
695
|
+
has_enums=bool(all_enums),
|
696
|
+
enum_names=sorted(all_enums.keys()) if all_enums else [],
|
697
|
+
api_class=api_class
|
698
|
+
)
|
699
|
+
|
700
|
+
return GeneratedFile(
|
701
|
+
path="__init__.py",
|
702
|
+
content=content,
|
703
|
+
description="Package exports with API class and JWT management",
|
704
|
+
)
|
705
|
+
|
706
|
+
def _generate_api_wrapper_class_python(self, tags: list[str]) -> str:
|
707
|
+
"""Generate API wrapper class with JWT management for Python."""
|
708
|
+
# Prepare property data
|
709
|
+
properties_data = []
|
710
|
+
for tag in tags:
|
711
|
+
properties_data.append({
|
712
|
+
"tag": tag,
|
713
|
+
"class_name": self.tag_to_class_name(tag),
|
714
|
+
"property": self.tag_to_property_name(tag),
|
715
|
+
})
|
716
|
+
|
717
|
+
template = self.jinja_env.get_template('python/api_wrapper.py.jinja')
|
718
|
+
return template.render(properties=properties_data)
|
719
|
+
|
720
|
+
def _generate_logger_file(self) -> GeneratedFile:
|
721
|
+
"""Generate logger.py with Rich integration."""
|
722
|
+
template = self.jinja_env.get_template('python/utils/logger.py.jinja')
|
723
|
+
content = template.render()
|
724
|
+
|
725
|
+
return GeneratedFile(
|
726
|
+
path="logger.py",
|
727
|
+
content=content,
|
728
|
+
description="API Logger with Rich",
|
729
|
+
)
|
730
|
+
|
731
|
+
def _generate_schema_file(self) -> GeneratedFile:
|
732
|
+
"""Generate schema.py with OpenAPI schema as dict."""
|
733
|
+
import json
|
734
|
+
import re
|
735
|
+
|
736
|
+
# First, convert to pretty JSON
|
737
|
+
schema_json = json.dumps(self.openapi_schema, indent=4, ensure_ascii=False)
|
738
|
+
|
739
|
+
# Convert JSON literals to Python literals
|
740
|
+
schema_json = re.sub(r'\btrue\b', 'True', schema_json)
|
741
|
+
schema_json = re.sub(r'\bfalse\b', 'False', schema_json)
|
742
|
+
schema_json = re.sub(r'\bnull\b', 'None', schema_json)
|
743
|
+
|
744
|
+
template = self.jinja_env.get_template('python/utils/schema.py.jinja')
|
745
|
+
content = template.render(schema_dict=schema_json)
|
746
|
+
|
747
|
+
return GeneratedFile(
|
748
|
+
path="schema.py",
|
749
|
+
content=content,
|
750
|
+
description="OpenAPI Schema",
|
751
|
+
)
|