django-cfg 1.4.10__py3-none-any.whl → 1.4.13__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/views/api/currencies.py +49 -6
- 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 +73 -49
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -348
- 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 +162 -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 +208 -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 +838 -0
- django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
- django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
- django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
- django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
- django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
- django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
- django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
- django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
- django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
- django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
- django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
- django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -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 +427 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/pytest.ini +30 -0
- django_cfg/modules/django_client/spectacular/__init__.py +10 -0
- django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
- django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
- django_cfg/modules/django_dashboard/sections/documentation.py +391 -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/LOGGING_GUIDE.md +1 -1
- 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 +21 -10
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- 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.10.dist-info → django_cfg-1.4.13.dist-info}/METADATA +2 -2
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
- django_cfg/management/commands/generate.py +0 -107
- /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,838 @@
|
|
1
|
+
"""
|
2
|
+
Base Generator - Common code generation logic.
|
3
|
+
|
4
|
+
This module defines the abstract BaseGenerator class that provides
|
5
|
+
common functionality for all code generators (Python, TypeScript, etc.).
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from abc import ABC, abstractmethod
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Any
|
13
|
+
|
14
|
+
from ..ir import IRContext, IROperationObject, IRSchemaObject
|
15
|
+
|
16
|
+
|
17
|
+
class GeneratedFile:
|
18
|
+
"""
|
19
|
+
Represents a generated file.
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
path: Relative file path (e.g., 'models.py', 'client.ts')
|
23
|
+
content: Generated file content
|
24
|
+
description: Human-readable description
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, path: str, content: str, description: str | None = None):
|
28
|
+
self.path = path
|
29
|
+
self.content = content
|
30
|
+
self.description = description
|
31
|
+
|
32
|
+
def __repr__(self) -> str:
|
33
|
+
return f"GeneratedFile(path={self.path!r}, size={len(self.content)} bytes)"
|
34
|
+
|
35
|
+
|
36
|
+
class BaseGenerator(ABC):
|
37
|
+
"""
|
38
|
+
Abstract base generator for IR → Code conversion.
|
39
|
+
|
40
|
+
Subclasses implement language-specific generation:
|
41
|
+
- PythonGenerator: Generates Python client (Pydantic 2 + httpx)
|
42
|
+
- TypeScriptGenerator: Generates TypeScript client (Fetch API)
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
context: IRContext,
|
48
|
+
client_structure: str = "namespaced",
|
49
|
+
openapi_schema: dict | None = None,
|
50
|
+
tag_prefix: str = "",
|
51
|
+
package_config: dict | None = None,
|
52
|
+
generate_package_files: bool = False,
|
53
|
+
generate_zod_schemas: bool = False,
|
54
|
+
generate_fetchers: bool = False,
|
55
|
+
generate_swr_hooks: bool = False,
|
56
|
+
):
|
57
|
+
"""
|
58
|
+
Initialize generator with IR context.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
context: IRContext from parser
|
62
|
+
client_structure: Client structure ("flat" or "namespaced")
|
63
|
+
openapi_schema: OpenAPI schema dict (for embedding in client)
|
64
|
+
tag_prefix: Prefix to add to all tag names (e.g., "cfg_")
|
65
|
+
package_config: Package configuration (name, version, author, etc.)
|
66
|
+
generate_package_files: Whether to generate package.json/pyproject.toml
|
67
|
+
generate_zod_schemas: Whether to generate Zod schemas (TypeScript only)
|
68
|
+
generate_fetchers: Whether to generate typed fetchers (TypeScript only)
|
69
|
+
generate_swr_hooks: Whether to generate SWR hooks (TypeScript only, React)
|
70
|
+
"""
|
71
|
+
self.context = context
|
72
|
+
self.client_structure = client_structure
|
73
|
+
self.openapi_schema = openapi_schema
|
74
|
+
self.tag_prefix = tag_prefix
|
75
|
+
self.package_config = package_config or {}
|
76
|
+
self.generate_package_files = generate_package_files
|
77
|
+
self.generate_zod_schemas = generate_zod_schemas
|
78
|
+
self.generate_fetchers = generate_fetchers
|
79
|
+
self.generate_swr_hooks = generate_swr_hooks
|
80
|
+
|
81
|
+
# ===== Namespaced Structure Helpers =====
|
82
|
+
|
83
|
+
def group_operations_by_tag(self) -> dict[str, list[IROperationObject]]:
|
84
|
+
"""
|
85
|
+
Group operations by their first tag with case-insensitive normalization.
|
86
|
+
|
87
|
+
Tags are normalized to prevent duplicates caused by case differences
|
88
|
+
(e.g., "Profiles" and "profiles" are treated as the same tag).
|
89
|
+
The canonical tag (first encountered) is preserved for display purposes.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Dictionary mapping canonical tag names to lists of operations.
|
93
|
+
Operations without tags are grouped under "default".
|
94
|
+
|
95
|
+
Examples:
|
96
|
+
>>> ops = generator.group_operations_by_tag()
|
97
|
+
>>> ops["users"] # [list_op, create_op, retrieve_op, ...]
|
98
|
+
>>> # "Users" and "users" are merged into one group with canonical "Users"
|
99
|
+
"""
|
100
|
+
from collections import defaultdict
|
101
|
+
|
102
|
+
ops_by_tag = defaultdict(list)
|
103
|
+
tag_canonical = {} # normalized_tag -> canonical_tag mapping
|
104
|
+
tag_variants = defaultdict(set) # Track all variants for warnings
|
105
|
+
|
106
|
+
for op_id, operation in self.context.operations.items():
|
107
|
+
tag = operation.tags[0] if operation.tags else "default"
|
108
|
+
|
109
|
+
# Normalize tag to lowercase for comparison
|
110
|
+
normalized_tag = tag.lower()
|
111
|
+
|
112
|
+
# Track all case variants
|
113
|
+
tag_variants[normalized_tag].add(tag)
|
114
|
+
|
115
|
+
# Use first encountered version as canonical
|
116
|
+
if normalized_tag not in tag_canonical:
|
117
|
+
tag_canonical[normalized_tag] = tag
|
118
|
+
|
119
|
+
# Group under canonical tag
|
120
|
+
canonical = tag_canonical[normalized_tag]
|
121
|
+
ops_by_tag[canonical].append(operation)
|
122
|
+
|
123
|
+
# Warn about case inconsistencies
|
124
|
+
for normalized, variants in tag_variants.items():
|
125
|
+
if len(variants) > 1:
|
126
|
+
canonical = tag_canonical[normalized]
|
127
|
+
other_variants = sorted(variants - {canonical})
|
128
|
+
print(f"⚠️ Warning: Found case variants of tag '{canonical}': {other_variants}")
|
129
|
+
print(f" → Using '{canonical}' as canonical tag")
|
130
|
+
|
131
|
+
return dict(ops_by_tag)
|
132
|
+
|
133
|
+
def tag_to_class_name(self, tag: str, suffix: str = "API") -> str:
|
134
|
+
"""
|
135
|
+
Convert tag to PascalCase class name.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
tag: Tag name (e.g., "users", "user-management", "django_cfg.auth")
|
139
|
+
suffix: Class name suffix (default: "API")
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
PascalCase class name with suffix and optional prefix
|
143
|
+
|
144
|
+
Examples:
|
145
|
+
>>> generator.tag_to_class_name("users")
|
146
|
+
'UsersAPI'
|
147
|
+
>>> generator.tag_to_class_name("user-management")
|
148
|
+
'UserManagementAPI'
|
149
|
+
>>> generator.tag_to_class_name("django_cfg.auth")
|
150
|
+
'DjangoCfgAuthAPI'
|
151
|
+
>>> generator = BaseGenerator(context, tag_prefix="cfg_")
|
152
|
+
>>> generator.tag_to_class_name("auth")
|
153
|
+
'CfgAuthAPI'
|
154
|
+
"""
|
155
|
+
# Use tag_to_property_name to get normalized name with prefix
|
156
|
+
normalized = self.tag_to_property_name(tag)
|
157
|
+
# Split into words and capitalize
|
158
|
+
words = normalized.split('_')
|
159
|
+
return ''.join(word.capitalize() for word in words if word) + suffix
|
160
|
+
|
161
|
+
def tag_to_property_name(self, tag: str) -> str:
|
162
|
+
"""
|
163
|
+
Convert tag to valid property/variable name.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
tag: Tag name (e.g., "Blog - Categories", "user-management", "django_cfg.leads")
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
Valid identifier (snake_case for Python, camelCase for TS) with optional prefix
|
170
|
+
|
171
|
+
Examples:
|
172
|
+
>>> generator.tag_to_property_name("Blog - Categories")
|
173
|
+
'blog_categories'
|
174
|
+
>>> generator.tag_to_property_name("user-management")
|
175
|
+
'user_management'
|
176
|
+
>>> generator = BaseGenerator(context, tag_prefix="cfg_")
|
177
|
+
>>> generator.tag_to_property_name("auth")
|
178
|
+
'cfg_auth'
|
179
|
+
>>> generator.tag_to_property_name("django_cfg.leads")
|
180
|
+
'cfg_leads' # django_cfg prefix is stripped before adding group prefix
|
181
|
+
"""
|
182
|
+
from django.utils.text import slugify
|
183
|
+
normalized = slugify(tag).replace('-', '_')
|
184
|
+
|
185
|
+
# Strip common app label prefixes to avoid duplication
|
186
|
+
# (e.g., "django_cfg_leads" → "leads" before adding group prefix)
|
187
|
+
prefixes_to_strip = ['django_cfg_', 'django_cfg.']
|
188
|
+
for prefix in prefixes_to_strip:
|
189
|
+
prefix_normalized = slugify(prefix).replace('-', '_')
|
190
|
+
if normalized.startswith(prefix_normalized):
|
191
|
+
normalized = normalized[len(prefix_normalized):]
|
192
|
+
break
|
193
|
+
|
194
|
+
# Strip leading underscores after prefix removal
|
195
|
+
# (e.g., "django_cfg.accounts" → "django_cfg_accounts" → "_accounts" → "accounts")
|
196
|
+
normalized = normalized.lstrip('_')
|
197
|
+
|
198
|
+
# Add group prefix if configured and not already present
|
199
|
+
if self.tag_prefix:
|
200
|
+
# Check if tag already starts with prefix (avoid duplication)
|
201
|
+
if not normalized.startswith(self.tag_prefix):
|
202
|
+
normalized = f"{self.tag_prefix}{normalized}"
|
203
|
+
|
204
|
+
return normalized
|
205
|
+
|
206
|
+
def extract_app_from_path(self, path: str) -> str | None:
|
207
|
+
"""
|
208
|
+
Extract Django app name from URL path.
|
209
|
+
|
210
|
+
Args:
|
211
|
+
path: URL path (e.g., "/django_cfg_leads/leads/", "/django_cfg_newsletter/campaigns/")
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
App name without trailing slash, or None if no app detected
|
215
|
+
|
216
|
+
Examples:
|
217
|
+
>>> generator.extract_app_from_path("/django_cfg_leads/leads/")
|
218
|
+
'django_cfg_leads'
|
219
|
+
>>> generator.extract_app_from_path("/django_cfg_newsletter/campaigns/")
|
220
|
+
'django_cfg_newsletter'
|
221
|
+
>>> generator.extract_app_from_path("/api/users/")
|
222
|
+
'api'
|
223
|
+
"""
|
224
|
+
# Remove leading/trailing slashes and split
|
225
|
+
parts = path.strip('/').split('/')
|
226
|
+
|
227
|
+
# First part is usually the app name
|
228
|
+
if parts:
|
229
|
+
return parts[0]
|
230
|
+
|
231
|
+
return None
|
232
|
+
|
233
|
+
def tag_and_app_to_folder_name(self, tag: str, operations: list) -> str:
|
234
|
+
"""
|
235
|
+
Generate folder name from tag and app name with smart deduplication.
|
236
|
+
|
237
|
+
When tag matches app name, the redundant tag portion is omitted to avoid
|
238
|
+
folder names like 'cfg__tasks__tasks'. This keeps naming clean and intuitive.
|
239
|
+
|
240
|
+
Args:
|
241
|
+
tag: Tag name (e.g., "Campaigns", "Tasks", "django_cfg.leads")
|
242
|
+
operations: List of operations to extract app name from
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
Folder name in one of these formats:
|
246
|
+
- group__app__tag (when tag differs from app)
|
247
|
+
- group__app (when tag matches app)
|
248
|
+
- group (when all three match - rare case)
|
249
|
+
|
250
|
+
Examples:
|
251
|
+
>>> # Distinct tag and app
|
252
|
+
>>> # operations with path="/django_cfg_newsletter/campaigns/"
|
253
|
+
>>> generator.tag_and_app_to_folder_name("Campaigns", operations)
|
254
|
+
'cfg__newsletter__campaigns'
|
255
|
+
|
256
|
+
>>> # Tag matches app (deduplication applied)
|
257
|
+
>>> # operations with path="/tasks/api/..."
|
258
|
+
>>> generator.tag_and_app_to_folder_name("Tasks", operations)
|
259
|
+
'cfg__tasks'
|
260
|
+
|
261
|
+
>>> # Tag matches app (deduplication applied)
|
262
|
+
>>> # operations with path="/django_cfg_leads/leads/"
|
263
|
+
>>> generator.tag_and_app_to_folder_name("Leads", operations)
|
264
|
+
'cfg__leads'
|
265
|
+
|
266
|
+
>>> # Triple match - all same (rare)
|
267
|
+
>>> # operations with path="/profiles/profiles/"
|
268
|
+
>>> generator.tag_and_app_to_folder_name("Profiles", operations) # group=profiles
|
269
|
+
'profiles'
|
270
|
+
"""
|
271
|
+
from django.utils.text import slugify
|
272
|
+
|
273
|
+
# Extract app name from first operation's path
|
274
|
+
app_name = None
|
275
|
+
if operations:
|
276
|
+
app_name = self.extract_app_from_path(operations[0].path)
|
277
|
+
|
278
|
+
if not app_name:
|
279
|
+
# Fallback: just use normalized tag
|
280
|
+
return self.tag_to_property_name(tag)
|
281
|
+
|
282
|
+
# Normalize app name (strip django_cfg prefix)
|
283
|
+
normalized_app = slugify(app_name).replace('-', '_')
|
284
|
+
prefixes_to_strip = ['django_cfg_', 'django_cfg.']
|
285
|
+
for prefix in prefixes_to_strip:
|
286
|
+
prefix_normalized = slugify(prefix).replace('-', '_')
|
287
|
+
if normalized_app.startswith(prefix_normalized):
|
288
|
+
normalized_app = normalized_app[len(prefix_normalized):]
|
289
|
+
break
|
290
|
+
normalized_app = normalized_app.lstrip('_')
|
291
|
+
|
292
|
+
# Normalize tag (strip django_cfg prefix)
|
293
|
+
normalized_tag = slugify(tag).replace('-', '_')
|
294
|
+
for prefix in prefixes_to_strip:
|
295
|
+
prefix_normalized = slugify(prefix).replace('-', '_')
|
296
|
+
if normalized_tag.startswith(prefix_normalized):
|
297
|
+
normalized_tag = normalized_tag[len(prefix_normalized):]
|
298
|
+
break
|
299
|
+
normalized_tag = normalized_tag.lstrip('_')
|
300
|
+
|
301
|
+
# Smart deduplication: if tag matches app, skip tag portion
|
302
|
+
if normalized_tag == normalized_app:
|
303
|
+
# Tag is redundant with app name
|
304
|
+
if self.tag_prefix:
|
305
|
+
group = self.tag_prefix.rstrip('_')
|
306
|
+
# Check if group also matches app (triple redundancy)
|
307
|
+
if group == normalized_app:
|
308
|
+
# All three are the same, just use group
|
309
|
+
return group
|
310
|
+
else:
|
311
|
+
# Tag matches app but not group: use group__app
|
312
|
+
return f"{group}__{normalized_app}"
|
313
|
+
else:
|
314
|
+
# No group prefix, just use app name
|
315
|
+
return normalized_app
|
316
|
+
|
317
|
+
# Build folder name: group__app__tag (when tag is distinct)
|
318
|
+
if self.tag_prefix:
|
319
|
+
# tag_prefix already has trailing underscore: "cfg_"
|
320
|
+
group = self.tag_prefix.rstrip('_')
|
321
|
+
return f"{group}__{normalized_app}__{normalized_tag}"
|
322
|
+
else:
|
323
|
+
return f"{normalized_app}__{normalized_tag}"
|
324
|
+
|
325
|
+
def tag_to_display_name(self, tag: str) -> str:
|
326
|
+
"""
|
327
|
+
Convert tag to human-readable display name for docstrings.
|
328
|
+
|
329
|
+
This method strips common prefixes and formats the tag for display in
|
330
|
+
documentation without adding group prefixes.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
tag: Tag name (e.g., "django_cfg.leads", "Campaigns", "User Profile")
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
Human-readable tag name (title case)
|
337
|
+
|
338
|
+
Examples:
|
339
|
+
>>> generator.tag_to_display_name("Campaigns")
|
340
|
+
'Campaigns'
|
341
|
+
>>> generator.tag_to_display_name("django_cfg.leads")
|
342
|
+
'Leads'
|
343
|
+
>>> generator.tag_to_display_name("django_cfg_accounts")
|
344
|
+
'Accounts'
|
345
|
+
>>> generator.tag_to_display_name("user-management")
|
346
|
+
'User Management'
|
347
|
+
"""
|
348
|
+
from django.utils.text import slugify
|
349
|
+
|
350
|
+
# If tag is already in title case with spaces, return as-is
|
351
|
+
if ' ' in tag and tag[0].isupper():
|
352
|
+
return tag
|
353
|
+
|
354
|
+
# Normalize the tag
|
355
|
+
normalized = slugify(tag).replace('-', '_')
|
356
|
+
|
357
|
+
# Strip common app label prefixes
|
358
|
+
prefixes_to_strip = ['django_cfg_', 'django_cfg.']
|
359
|
+
for prefix in prefixes_to_strip:
|
360
|
+
prefix_normalized = slugify(prefix).replace('-', '_')
|
361
|
+
if normalized.startswith(prefix_normalized):
|
362
|
+
normalized = normalized[len(prefix_normalized):]
|
363
|
+
break
|
364
|
+
|
365
|
+
# Strip leading underscores
|
366
|
+
normalized = normalized.lstrip('_')
|
367
|
+
|
368
|
+
# Convert to title case with spaces
|
369
|
+
words = normalized.split('_')
|
370
|
+
return ' '.join(word.capitalize() for word in words if word)
|
371
|
+
|
372
|
+
def remove_tag_prefix(self, operation_id: str, tag: str) -> str:
|
373
|
+
"""
|
374
|
+
Remove tag prefix from operation_id.
|
375
|
+
|
376
|
+
This method handles complex operation ID patterns by:
|
377
|
+
1. Stripping common app label prefixes (django_cfg_*, etc.)
|
378
|
+
2. Attempting to strip the normalized tag name
|
379
|
+
3. Returning the cleaned operation_id
|
380
|
+
|
381
|
+
Args:
|
382
|
+
operation_id: Operation ID (e.g., "django_cfg_newsletter_campaigns_list", "posts_list")
|
383
|
+
tag: Tag name (e.g., "Campaigns", "posts", "django_cfg.accounts")
|
384
|
+
|
385
|
+
Returns:
|
386
|
+
Operation ID without tag prefix
|
387
|
+
|
388
|
+
Examples:
|
389
|
+
>>> generator.remove_tag_prefix("posts_list", "posts")
|
390
|
+
'list'
|
391
|
+
>>> generator.remove_tag_prefix("django_cfg_newsletter_campaigns_list", "Campaigns")
|
392
|
+
'list'
|
393
|
+
>>> generator.remove_tag_prefix("django_cfg_accounts_token_refresh_create", "Auth")
|
394
|
+
'token_refresh_create'
|
395
|
+
>>> generator.remove_tag_prefix("retrieve", "users")
|
396
|
+
'retrieve' # No prefix to remove
|
397
|
+
"""
|
398
|
+
from django.utils.text import slugify
|
399
|
+
|
400
|
+
# First, strip common app label prefixes from operation_id
|
401
|
+
# This handles cases like "django_cfg_newsletter_campaigns_list"
|
402
|
+
cleaned_op_id = operation_id
|
403
|
+
app_prefixes_to_strip = [
|
404
|
+
'django_cfg_newsletter_',
|
405
|
+
'django_cfg_accounts_',
|
406
|
+
'django_cfg_leads_',
|
407
|
+
'django_cfg_support_',
|
408
|
+
'django_cfg_agents_',
|
409
|
+
'django_cfg_knowbase_',
|
410
|
+
'django_cfg_payments_',
|
411
|
+
'django_cfg_tasks_',
|
412
|
+
'django_cfg_', # Catch-all for any django_cfg_* prefixes
|
413
|
+
]
|
414
|
+
|
415
|
+
for app_prefix in app_prefixes_to_strip:
|
416
|
+
if cleaned_op_id.startswith(app_prefix):
|
417
|
+
cleaned_op_id = cleaned_op_id[len(app_prefix):]
|
418
|
+
break
|
419
|
+
|
420
|
+
# Now try to remove the normalized tag as a prefix
|
421
|
+
# Normalize tag same way as tag_to_property_name but without adding group prefix
|
422
|
+
normalized_tag = slugify(tag).replace('-', '_')
|
423
|
+
|
424
|
+
# Strip django_cfg prefix from tag too
|
425
|
+
tag_prefixes = ['django_cfg_', 'django_cfg.']
|
426
|
+
for prefix in tag_prefixes:
|
427
|
+
prefix_normalized = slugify(prefix).replace('-', '_')
|
428
|
+
if normalized_tag.startswith(prefix_normalized):
|
429
|
+
normalized_tag = normalized_tag[len(prefix_normalized):]
|
430
|
+
break
|
431
|
+
|
432
|
+
# Strip leading underscores from tag
|
433
|
+
normalized_tag = normalized_tag.lstrip('_')
|
434
|
+
|
435
|
+
# Try to remove normalized tag as prefix
|
436
|
+
tag_prefix = f"{normalized_tag}_"
|
437
|
+
if cleaned_op_id.startswith(tag_prefix):
|
438
|
+
cleaned_op_id = cleaned_op_id[len(tag_prefix):]
|
439
|
+
|
440
|
+
return cleaned_op_id
|
441
|
+
|
442
|
+
# ===== Main Generate Method =====
|
443
|
+
|
444
|
+
@abstractmethod
|
445
|
+
def generate(self) -> list[GeneratedFile]:
|
446
|
+
"""
|
447
|
+
Generate all client files.
|
448
|
+
|
449
|
+
Returns:
|
450
|
+
List of GeneratedFile objects
|
451
|
+
|
452
|
+
Examples:
|
453
|
+
>>> generator = PythonGenerator(context)
|
454
|
+
>>> files = generator.generate()
|
455
|
+
>>> for file in files:
|
456
|
+
... print(f"{file.path}: {len(file.content)} bytes")
|
457
|
+
models.py: 1234 bytes
|
458
|
+
client.py: 5678 bytes
|
459
|
+
"""
|
460
|
+
pass
|
461
|
+
|
462
|
+
# ===== Schema Generation (Abstract) =====
|
463
|
+
|
464
|
+
@abstractmethod
|
465
|
+
def generate_schema(self, schema: IRSchemaObject) -> str:
|
466
|
+
"""
|
467
|
+
Generate code for a single schema.
|
468
|
+
|
469
|
+
Args:
|
470
|
+
schema: IRSchemaObject to generate
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
Generated code (class definition, interface, etc.)
|
474
|
+
|
475
|
+
Examples:
|
476
|
+
>>> schema = IRSchemaObject(name="User", type="object", ...)
|
477
|
+
>>> code = generator.generate_schema(schema)
|
478
|
+
>>> # Python: "class User(BaseModel): ..."
|
479
|
+
>>> # TypeScript: "interface User { ... }"
|
480
|
+
"""
|
481
|
+
pass
|
482
|
+
|
483
|
+
@abstractmethod
|
484
|
+
def generate_enum(self, schema: IRSchemaObject) -> str:
|
485
|
+
"""
|
486
|
+
Generate enum code from schema with x-enum-varnames.
|
487
|
+
|
488
|
+
Args:
|
489
|
+
schema: IRSchemaObject with enum + enum_var_names
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
Generated enum code
|
493
|
+
|
494
|
+
Examples:
|
495
|
+
>>> schema = IRSchemaObject(
|
496
|
+
... name="StatusEnum",
|
497
|
+
... type="integer",
|
498
|
+
... enum=[1, 2, 3],
|
499
|
+
... enum_var_names=["STATUS_NEW", "STATUS_IN_PROGRESS", "STATUS_COMPLETE"]
|
500
|
+
... )
|
501
|
+
>>> code = generator.generate_enum(schema)
|
502
|
+
>>> # Python: "class StatusEnum(IntEnum): ..."
|
503
|
+
>>> # TypeScript: "enum StatusEnum { ... }"
|
504
|
+
"""
|
505
|
+
pass
|
506
|
+
|
507
|
+
# ===== Operation Generation (Abstract) =====
|
508
|
+
|
509
|
+
@abstractmethod
|
510
|
+
def generate_operation(self, operation: IROperationObject) -> str:
|
511
|
+
"""
|
512
|
+
Generate code for a single operation (endpoint method).
|
513
|
+
|
514
|
+
Args:
|
515
|
+
operation: IROperationObject to generate
|
516
|
+
|
517
|
+
Returns:
|
518
|
+
Generated method code
|
519
|
+
|
520
|
+
Examples:
|
521
|
+
>>> operation = IROperationObject(
|
522
|
+
... operation_id="users_list",
|
523
|
+
... http_method="GET",
|
524
|
+
... path="/api/users/",
|
525
|
+
... ...
|
526
|
+
... )
|
527
|
+
>>> code = generator.generate_operation(operation)
|
528
|
+
>>> # Python: "async def users_list(self, ...) -> list[User]: ..."
|
529
|
+
>>> # TypeScript: "async usersList(...): Promise<User[]> { ... }"
|
530
|
+
"""
|
531
|
+
pass
|
532
|
+
|
533
|
+
# ===== Helpers =====
|
534
|
+
|
535
|
+
def get_request_schemas(self) -> dict[str, IRSchemaObject]:
|
536
|
+
"""Get all request schemas (UserRequest, etc.)."""
|
537
|
+
return self.context.request_models
|
538
|
+
|
539
|
+
def get_response_schemas(self) -> dict[str, IRSchemaObject]:
|
540
|
+
"""Get all response schemas (User, etc.)."""
|
541
|
+
return self.context.response_models
|
542
|
+
|
543
|
+
def get_patch_schemas(self) -> dict[str, IRSchemaObject]:
|
544
|
+
"""Get all PATCH schemas (PatchedUser, etc.)."""
|
545
|
+
return self.context.patch_models
|
546
|
+
|
547
|
+
def get_enum_schemas(self) -> dict[str, IRSchemaObject]:
|
548
|
+
"""Get all enum schemas with x-enum-varnames."""
|
549
|
+
return self.context.enum_schemas
|
550
|
+
|
551
|
+
def _collect_enums_from_schemas(
|
552
|
+
self, schemas: dict[str, IRSchemaObject]
|
553
|
+
) -> dict[str, IRSchemaObject]:
|
554
|
+
"""
|
555
|
+
Recursively collect all enum schemas used by given schemas.
|
556
|
+
|
557
|
+
This method extracts enums from:
|
558
|
+
- Top-level enum schemas (StatusEnum as a component)
|
559
|
+
- Nested enum properties (User.status where status has enum)
|
560
|
+
- Array items with enums (Task.tags where tags[0] is enum)
|
561
|
+
|
562
|
+
Auto-generates enum_var_names if not present:
|
563
|
+
- "open" → "OPEN"
|
564
|
+
- "waiting_for_user" → "WAITING_FOR_USER"
|
565
|
+
|
566
|
+
Args:
|
567
|
+
schemas: Dictionary of schemas to scan
|
568
|
+
|
569
|
+
Returns:
|
570
|
+
Dictionary of enum schemas found (with auto-generated var names)
|
571
|
+
|
572
|
+
Examples:
|
573
|
+
>>> schemas = {
|
574
|
+
... "Payment": IRSchemaObject(
|
575
|
+
... name="Payment",
|
576
|
+
... type="object",
|
577
|
+
... properties={
|
578
|
+
... "status": IRSchemaObject(
|
579
|
+
... name="StatusEnum",
|
580
|
+
... type="string",
|
581
|
+
... enum=["open", "closed"]
|
582
|
+
... )
|
583
|
+
... }
|
584
|
+
... )
|
585
|
+
... }
|
586
|
+
>>> enums = generator._collect_enums_from_schemas(schemas)
|
587
|
+
>>> enums["StatusEnum"].enum_var_names
|
588
|
+
["OPEN", "CLOSED"]
|
589
|
+
"""
|
590
|
+
enums = {}
|
591
|
+
|
592
|
+
def auto_generate_enum_var_names(schema: IRSchemaObject) -> IRSchemaObject:
|
593
|
+
"""Auto-generate enum_var_names from enum values if missing."""
|
594
|
+
if schema.enum and not schema.enum_var_names:
|
595
|
+
# Generate variable names from values
|
596
|
+
var_names = []
|
597
|
+
for value in schema.enum:
|
598
|
+
if isinstance(value, str):
|
599
|
+
# Convert "waiting_for_user" → "WAITING_FOR_USER"
|
600
|
+
var_name = value.upper().replace("-", "_").replace(" ", "_")
|
601
|
+
else:
|
602
|
+
# For integers: 1 → "VALUE_1"
|
603
|
+
var_name = f"VALUE_{value}"
|
604
|
+
var_names.append(var_name)
|
605
|
+
|
606
|
+
# Create new schema with auto-generated var names
|
607
|
+
schema = IRSchemaObject(
|
608
|
+
**{**schema.model_dump(), "enum_var_names": var_names}
|
609
|
+
)
|
610
|
+
return schema
|
611
|
+
|
612
|
+
def collect_recursive(schema: IRSchemaObject):
|
613
|
+
"""Recursively collect enums from schema and its nested properties."""
|
614
|
+
# Check if this schema itself is an enum (with or without x-enum-varnames)
|
615
|
+
if schema.enum and schema.name:
|
616
|
+
schema = auto_generate_enum_var_names(schema)
|
617
|
+
enums[schema.name] = schema
|
618
|
+
|
619
|
+
# Check if this schema is a reference to an enum
|
620
|
+
if schema.ref and schema.ref in self.context.schemas:
|
621
|
+
ref_schema = self.context.schemas[schema.ref]
|
622
|
+
if ref_schema.enum:
|
623
|
+
ref_schema = auto_generate_enum_var_names(ref_schema)
|
624
|
+
enums[ref_schema.name] = ref_schema
|
625
|
+
|
626
|
+
# Check properties for enums
|
627
|
+
if schema.properties:
|
628
|
+
for prop_schema in schema.properties.values():
|
629
|
+
# If property has enum, it's a standalone enum
|
630
|
+
if prop_schema.enum and prop_schema.name:
|
631
|
+
prop_schema = auto_generate_enum_var_names(prop_schema)
|
632
|
+
enums[prop_schema.name] = prop_schema
|
633
|
+
# Check if property is a reference to an enum
|
634
|
+
elif prop_schema.ref and prop_schema.ref in self.context.schemas:
|
635
|
+
ref_schema = self.context.schemas[prop_schema.ref]
|
636
|
+
if ref_schema.enum:
|
637
|
+
ref_schema = auto_generate_enum_var_names(ref_schema)
|
638
|
+
enums[ref_schema.name] = ref_schema
|
639
|
+
# Recurse into nested objects
|
640
|
+
elif prop_schema.type == "object":
|
641
|
+
collect_recursive(prop_schema)
|
642
|
+
# Recurse into arrays
|
643
|
+
elif prop_schema.type == "array" and prop_schema.items:
|
644
|
+
if prop_schema.items.enum and prop_schema.items.name:
|
645
|
+
items = auto_generate_enum_var_names(prop_schema.items)
|
646
|
+
enums[items.name] = items
|
647
|
+
elif prop_schema.items.ref and prop_schema.items.ref in self.context.schemas:
|
648
|
+
ref_items = self.context.schemas[prop_schema.items.ref]
|
649
|
+
if ref_items.enum:
|
650
|
+
ref_items = auto_generate_enum_var_names(ref_items)
|
651
|
+
enums[ref_items.name] = ref_items
|
652
|
+
elif prop_schema.items.type == "object":
|
653
|
+
collect_recursive(prop_schema.items)
|
654
|
+
|
655
|
+
# Check array items for enums (if schema itself is array)
|
656
|
+
if schema.items:
|
657
|
+
if schema.items.enum and schema.items.name:
|
658
|
+
items = auto_generate_enum_var_names(schema.items)
|
659
|
+
enums[items.name] = items
|
660
|
+
elif schema.items.ref and schema.items.ref in self.context.schemas:
|
661
|
+
ref_items = self.context.schemas[schema.items.ref]
|
662
|
+
if ref_items.enum:
|
663
|
+
ref_items = auto_generate_enum_var_names(ref_items)
|
664
|
+
enums[ref_items.name] = ref_items
|
665
|
+
elif schema.items.type == "object":
|
666
|
+
collect_recursive(schema.items)
|
667
|
+
|
668
|
+
# Collect enums from all schemas
|
669
|
+
for schema in schemas.values():
|
670
|
+
collect_recursive(schema)
|
671
|
+
|
672
|
+
return enums
|
673
|
+
|
674
|
+
def get_operations_by_tag(self) -> dict[str, list[IROperationObject]]:
|
675
|
+
"""Get operations grouped by tags."""
|
676
|
+
return self.context.operations_by_tag
|
677
|
+
|
678
|
+
def save_files(self, files: list[GeneratedFile], output_dir: Path) -> None:
|
679
|
+
"""
|
680
|
+
Save generated files to disk.
|
681
|
+
|
682
|
+
Args:
|
683
|
+
files: List of GeneratedFile objects
|
684
|
+
output_dir: Output directory path
|
685
|
+
|
686
|
+
Examples:
|
687
|
+
>>> files = generator.generate()
|
688
|
+
>>> generator.save_files(files, Path("./generated"))
|
689
|
+
"""
|
690
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
691
|
+
|
692
|
+
for file in files:
|
693
|
+
file_path = output_dir / file.path
|
694
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
695
|
+
|
696
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
697
|
+
f.write(file.content)
|
698
|
+
|
699
|
+
# ===== Formatting Helpers =====
|
700
|
+
|
701
|
+
def indent(self, code: str, spaces: int = 4) -> str:
|
702
|
+
"""
|
703
|
+
Indent code by N spaces.
|
704
|
+
|
705
|
+
Args:
|
706
|
+
code: Code to indent
|
707
|
+
spaces: Number of spaces (default: 4)
|
708
|
+
|
709
|
+
Returns:
|
710
|
+
Indented code
|
711
|
+
|
712
|
+
Examples:
|
713
|
+
>>> generator.indent("x = 1\\ny = 2", 4)
|
714
|
+
' x = 1\\n y = 2'
|
715
|
+
"""
|
716
|
+
lines = code.split("\n")
|
717
|
+
indent_str = " " * spaces
|
718
|
+
return "\n".join(f"{indent_str}{line}" if line.strip() else line for line in lines)
|
719
|
+
|
720
|
+
def sanitize_name(self, name: str) -> str:
|
721
|
+
"""
|
722
|
+
Sanitize schema/operation name to valid identifier.
|
723
|
+
|
724
|
+
Args:
|
725
|
+
name: Raw name
|
726
|
+
|
727
|
+
Returns:
|
728
|
+
Sanitized name
|
729
|
+
|
730
|
+
Examples:
|
731
|
+
>>> generator.sanitize_name("User-Profile")
|
732
|
+
'User_Profile'
|
733
|
+
>>> generator.sanitize_name("2Users")
|
734
|
+
'_2Users'
|
735
|
+
"""
|
736
|
+
# Replace invalid characters with underscore
|
737
|
+
sanitized = "".join(c if c.isalnum() or c == "_" else "_" for c in name)
|
738
|
+
|
739
|
+
# Ensure doesn't start with digit
|
740
|
+
if sanitized and sanitized[0].isdigit():
|
741
|
+
sanitized = f"_{sanitized}"
|
742
|
+
|
743
|
+
return sanitized or "Unknown"
|
744
|
+
|
745
|
+
def wrap_comment(self, text: str, max_length: int = 80) -> list[str]:
|
746
|
+
"""
|
747
|
+
Wrap long comment text to multiple lines.
|
748
|
+
|
749
|
+
Args:
|
750
|
+
text: Comment text
|
751
|
+
max_length: Maximum line length
|
752
|
+
|
753
|
+
Returns:
|
754
|
+
List of wrapped lines
|
755
|
+
|
756
|
+
Examples:
|
757
|
+
>>> generator.wrap_comment("This is a very long comment that should be wrapped", 30)
|
758
|
+
['This is a very long comment', 'that should be wrapped']
|
759
|
+
"""
|
760
|
+
if not text:
|
761
|
+
return []
|
762
|
+
|
763
|
+
words = text.split()
|
764
|
+
lines = []
|
765
|
+
current_line = []
|
766
|
+
current_length = 0
|
767
|
+
|
768
|
+
for word in words:
|
769
|
+
word_length = len(word) + (1 if current_line else 0)
|
770
|
+
|
771
|
+
if current_length + word_length > max_length and current_line:
|
772
|
+
lines.append(" ".join(current_line))
|
773
|
+
current_line = [word]
|
774
|
+
current_length = len(word)
|
775
|
+
else:
|
776
|
+
current_line.append(word)
|
777
|
+
current_length += word_length
|
778
|
+
|
779
|
+
if current_line:
|
780
|
+
lines.append(" ".join(current_line))
|
781
|
+
|
782
|
+
return lines
|
783
|
+
|
784
|
+
def format_enum_description(self, text: str) -> str:
|
785
|
+
"""
|
786
|
+
Format enum description by splitting bullet points.
|
787
|
+
|
788
|
+
Enum descriptions from OpenAPI often have the format:
|
789
|
+
"* `value1` - Desc1 * `value2` - Desc2"
|
790
|
+
|
791
|
+
This method splits them into separate lines:
|
792
|
+
"* `value1` - Desc1\n* `value2` - Desc2"
|
793
|
+
|
794
|
+
Args:
|
795
|
+
text: Enum description text
|
796
|
+
|
797
|
+
Returns:
|
798
|
+
Formatted description with proper line breaks
|
799
|
+
"""
|
800
|
+
if not text:
|
801
|
+
return text
|
802
|
+
|
803
|
+
# Split by " * `" pattern (preserving the first *)
|
804
|
+
import re
|
805
|
+
# Replace " * `" with newline + "* `"
|
806
|
+
formatted = re.sub(r'\s+\*\s+`', '\n* `', text.strip())
|
807
|
+
|
808
|
+
return formatted
|
809
|
+
|
810
|
+
def sanitize_enum_name(self, name: str) -> str:
|
811
|
+
"""
|
812
|
+
Sanitize enum name by converting to PascalCase.
|
813
|
+
|
814
|
+
Examples:
|
815
|
+
"OrderDetail.status" -> "OrderDetailStatus"
|
816
|
+
"Currency.currency_type" -> "CurrencyCurrencyType"
|
817
|
+
"CurrencyList.currency_type" -> "CurrencyListCurrencyType"
|
818
|
+
"User.role" -> "UserRole"
|
819
|
+
|
820
|
+
Args:
|
821
|
+
name: Original enum name (may contain dots, underscores)
|
822
|
+
|
823
|
+
Returns:
|
824
|
+
Sanitized PascalCase name
|
825
|
+
"""
|
826
|
+
# Replace dots with underscores, then split and convert to PascalCase
|
827
|
+
parts = name.replace('.', '_').split('_')
|
828
|
+
result = []
|
829
|
+
for word in parts:
|
830
|
+
if not word:
|
831
|
+
continue
|
832
|
+
# If word is already PascalCase/camelCase, keep it as is
|
833
|
+
# Otherwise capitalize first letter only
|
834
|
+
if word[0].isupper():
|
835
|
+
result.append(word)
|
836
|
+
else:
|
837
|
+
result.append(word[0].upper() + word[1:] if len(word) > 1 else word.upper())
|
838
|
+
return ''.join(result)
|