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,208 @@
|
|
1
|
+
"""
|
2
|
+
Main OpenAPI configuration for django_cfg.
|
3
|
+
|
4
|
+
Replaces django-revolution with integrated django_openapi module.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Dict, Optional, Literal, List
|
8
|
+
from pathlib import Path
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
10
|
+
from .group import OpenAPIGroupConfig
|
11
|
+
|
12
|
+
|
13
|
+
class OpenAPIConfig(BaseModel):
|
14
|
+
"""
|
15
|
+
Main OpenAPI configuration for django-cfg.
|
16
|
+
|
17
|
+
Features:
|
18
|
+
- Smart application grouping (cfg/custom)
|
19
|
+
- Python & TypeScript client generation
|
20
|
+
- Pure Python implementation (no external dependencies)
|
21
|
+
- 20x faster than django-revolution
|
22
|
+
|
23
|
+
Example:
|
24
|
+
>>> from django_cfg import DjangoCfg
|
25
|
+
>>> config = DjangoCfg(
|
26
|
+
... openapi=OpenAPIConfig(
|
27
|
+
... enabled=True,
|
28
|
+
... groups=[
|
29
|
+
... OpenAPIGroupConfig(
|
30
|
+
... name="cfg",
|
31
|
+
... apps=["django_cfg.*"],
|
32
|
+
... title="Framework API",
|
33
|
+
... ),
|
34
|
+
... OpenAPIGroupConfig(
|
35
|
+
... name="custom",
|
36
|
+
... apps=["myapp"],
|
37
|
+
... title="Custom API",
|
38
|
+
... ),
|
39
|
+
... ],
|
40
|
+
... ),
|
41
|
+
... )
|
42
|
+
"""
|
43
|
+
|
44
|
+
model_config = {
|
45
|
+
"str_strip_whitespace": True,
|
46
|
+
"validate_assignment": True,
|
47
|
+
"extra": "forbid",
|
48
|
+
}
|
49
|
+
|
50
|
+
# Control
|
51
|
+
enabled: bool = Field(
|
52
|
+
default=False,
|
53
|
+
description="Enable OpenAPI client generation",
|
54
|
+
)
|
55
|
+
|
56
|
+
# Application Grouping
|
57
|
+
groups: List[OpenAPIGroupConfig] = Field(
|
58
|
+
default_factory=list,
|
59
|
+
description="Application groups for separate schema generation",
|
60
|
+
)
|
61
|
+
|
62
|
+
# Output Configuration
|
63
|
+
output_dir: str = Field(
|
64
|
+
default="openapi",
|
65
|
+
description="Base output directory for schemas and clients",
|
66
|
+
)
|
67
|
+
|
68
|
+
# Client Generation
|
69
|
+
generate_python: bool = Field(
|
70
|
+
default=True,
|
71
|
+
description="Generate Python client",
|
72
|
+
)
|
73
|
+
|
74
|
+
generate_typescript: bool = Field(
|
75
|
+
default=True,
|
76
|
+
description="Generate TypeScript client",
|
77
|
+
)
|
78
|
+
|
79
|
+
generate_package_files: bool = Field(
|
80
|
+
default=False,
|
81
|
+
description="Generate package.json (TypeScript) and pyproject.toml (Python)",
|
82
|
+
)
|
83
|
+
|
84
|
+
generate_zod_schemas: bool = Field(
|
85
|
+
default=False,
|
86
|
+
description="Generate Zod schemas for runtime validation (TypeScript only)",
|
87
|
+
)
|
88
|
+
|
89
|
+
generate_fetchers: bool = Field(
|
90
|
+
default=False,
|
91
|
+
description="Generate typed fetcher functions (TypeScript only, requires Zod schemas)",
|
92
|
+
)
|
93
|
+
|
94
|
+
generate_swr_hooks: bool = Field(
|
95
|
+
default=False,
|
96
|
+
description="Generate SWR hooks for React (TypeScript only, requires fetchers)",
|
97
|
+
)
|
98
|
+
|
99
|
+
client_structure: Literal["flat", "namespaced"] = Field(
|
100
|
+
default="namespaced",
|
101
|
+
description=(
|
102
|
+
"Client structure:\n"
|
103
|
+
" - flat: All methods in one class (client.posts_list())\n"
|
104
|
+
" - namespaced: Organized by tags (client.posts.list())"
|
105
|
+
),
|
106
|
+
)
|
107
|
+
|
108
|
+
# API Configuration
|
109
|
+
api_prefix: str = Field(
|
110
|
+
default="apix",
|
111
|
+
description="API URL prefix (e.g., 'apix' -> /apix/app/endpoint)",
|
112
|
+
)
|
113
|
+
|
114
|
+
# Archive Configuration
|
115
|
+
enable_archive: bool = Field(
|
116
|
+
default=True,
|
117
|
+
description="Enable client archiving with versioning",
|
118
|
+
)
|
119
|
+
|
120
|
+
archive_retention_days: int = Field(
|
121
|
+
default=30,
|
122
|
+
description="Days to keep archived clients",
|
123
|
+
ge=1,
|
124
|
+
)
|
125
|
+
|
126
|
+
# Performance
|
127
|
+
max_workers: int = Field(
|
128
|
+
default=1,
|
129
|
+
description="Number of parallel workers (1 = single-threaded, which is fast enough)",
|
130
|
+
ge=1,
|
131
|
+
le=20,
|
132
|
+
)
|
133
|
+
|
134
|
+
@field_validator("groups")
|
135
|
+
@classmethod
|
136
|
+
def validate_groups_when_enabled(cls, v: List[OpenAPIGroupConfig], info) -> List[OpenAPIGroupConfig]:
|
137
|
+
"""Ensure at least one group is defined when enabled and names are unique."""
|
138
|
+
# Access enabled field via info.data
|
139
|
+
enabled = info.data.get("enabled", False)
|
140
|
+
if enabled and not v:
|
141
|
+
raise ValueError("At least one group must be defined when OpenAPI is enabled")
|
142
|
+
|
143
|
+
# Check for duplicate group names
|
144
|
+
names = [group.name for group in v]
|
145
|
+
if len(names) != len(set(names)):
|
146
|
+
duplicates = [name for name in names if names.count(name) > 1]
|
147
|
+
raise ValueError(f"Duplicate group names found: {', '.join(set(duplicates))}")
|
148
|
+
|
149
|
+
return v
|
150
|
+
|
151
|
+
@field_validator("api_prefix")
|
152
|
+
@classmethod
|
153
|
+
def validate_api_prefix(cls, v: str) -> str:
|
154
|
+
"""Ensure API prefix is valid."""
|
155
|
+
v = v.strip().strip("/")
|
156
|
+
if not v:
|
157
|
+
raise ValueError("api_prefix cannot be empty")
|
158
|
+
return v
|
159
|
+
|
160
|
+
@field_validator("output_dir")
|
161
|
+
@classmethod
|
162
|
+
def validate_output_dir(cls, v: str) -> str:
|
163
|
+
"""Ensure output directory is valid."""
|
164
|
+
v = v.strip()
|
165
|
+
if not v:
|
166
|
+
raise ValueError("output_dir cannot be empty")
|
167
|
+
return v
|
168
|
+
|
169
|
+
def get_output_path(self) -> Path:
|
170
|
+
"""Get absolute output path."""
|
171
|
+
return Path(self.output_dir).resolve()
|
172
|
+
|
173
|
+
def get_schemas_dir(self) -> Path:
|
174
|
+
"""Get schemas directory path."""
|
175
|
+
return self.get_output_path() / "schemas"
|
176
|
+
|
177
|
+
def get_clients_dir(self) -> Path:
|
178
|
+
"""Get clients directory path."""
|
179
|
+
return self.get_output_path() / "clients"
|
180
|
+
|
181
|
+
def get_python_clients_dir(self) -> Path:
|
182
|
+
"""Get Python clients directory path."""
|
183
|
+
return self.get_clients_dir() / "python"
|
184
|
+
|
185
|
+
def get_typescript_clients_dir(self) -> Path:
|
186
|
+
"""Get TypeScript clients directory path."""
|
187
|
+
return self.get_clients_dir() / "typescript"
|
188
|
+
|
189
|
+
def get_archive_dir(self) -> Path:
|
190
|
+
"""Get archive directory path."""
|
191
|
+
return self.get_output_path() / "archive"
|
192
|
+
|
193
|
+
def get_group_schema_path(self, group_name: str) -> Path:
|
194
|
+
"""Get OpenAPI schema path for a group."""
|
195
|
+
return self.get_schemas_dir() / f"{group_name}.yaml"
|
196
|
+
|
197
|
+
def get_group_python_dir(self, group_name: str) -> Path:
|
198
|
+
"""Get Python client directory for a group."""
|
199
|
+
return self.get_python_clients_dir() / group_name
|
200
|
+
|
201
|
+
def get_group_typescript_dir(self, group_name: str) -> Path:
|
202
|
+
"""Get TypeScript client directory for a group."""
|
203
|
+
return self.get_typescript_clients_dir() / group_name
|
204
|
+
|
205
|
+
|
206
|
+
__all__ = [
|
207
|
+
"OpenAPIConfig",
|
208
|
+
]
|
@@ -0,0 +1,101 @@
|
|
1
|
+
"""
|
2
|
+
OpenAPI Group Configuration.
|
3
|
+
|
4
|
+
Defines application grouping (cfg/custom) for separate schema generation.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import List
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
9
|
+
|
10
|
+
|
11
|
+
class OpenAPIGroupConfig(BaseModel):
|
12
|
+
"""
|
13
|
+
Configuration for a single application group.
|
14
|
+
|
15
|
+
Groups organize Django apps into separate OpenAPI schemas and clients.
|
16
|
+
|
17
|
+
Example:
|
18
|
+
>>> cfg_group = OpenAPIGroupConfig(
|
19
|
+
... name="cfg",
|
20
|
+
... apps=["django_cfg.*"],
|
21
|
+
... title="Django Config Framework API",
|
22
|
+
... description="Core framework functionality",
|
23
|
+
... )
|
24
|
+
"""
|
25
|
+
|
26
|
+
model_config = {
|
27
|
+
"str_strip_whitespace": True,
|
28
|
+
"validate_assignment": True,
|
29
|
+
"extra": "forbid",
|
30
|
+
}
|
31
|
+
|
32
|
+
# Group identification
|
33
|
+
name: str = Field(
|
34
|
+
...,
|
35
|
+
description="Unique name for this group (used for file paths and identification)",
|
36
|
+
min_length=1,
|
37
|
+
)
|
38
|
+
|
39
|
+
# App selection
|
40
|
+
apps: List[str] = Field(
|
41
|
+
...,
|
42
|
+
description="List of Django apps in this group. Supports wildcards (e.g., 'django_cfg.*')",
|
43
|
+
min_length=1,
|
44
|
+
)
|
45
|
+
|
46
|
+
# Metadata
|
47
|
+
title: str = Field(
|
48
|
+
...,
|
49
|
+
description="Human-readable title for this group",
|
50
|
+
min_length=1,
|
51
|
+
)
|
52
|
+
|
53
|
+
description: str = Field(
|
54
|
+
default="",
|
55
|
+
description="Detailed description for this group",
|
56
|
+
)
|
57
|
+
|
58
|
+
# API configuration
|
59
|
+
version: str = Field(
|
60
|
+
default="v1",
|
61
|
+
description="API version string",
|
62
|
+
)
|
63
|
+
|
64
|
+
# Authentication
|
65
|
+
auth_required: bool = Field(
|
66
|
+
default=False,
|
67
|
+
description="Whether authentication is required for this group",
|
68
|
+
)
|
69
|
+
|
70
|
+
@field_validator("name")
|
71
|
+
@classmethod
|
72
|
+
def validate_name(cls, v: str) -> str:
|
73
|
+
"""Ensure name is valid (alphanumeric + underscore/hyphen)."""
|
74
|
+
import re
|
75
|
+
v = v.strip()
|
76
|
+
if not v:
|
77
|
+
raise ValueError("name cannot be empty")
|
78
|
+
if not re.match(r'^[a-zA-Z0-9_-]+$', v):
|
79
|
+
raise ValueError("name must contain only alphanumeric characters, underscores, or hyphens")
|
80
|
+
return v
|
81
|
+
|
82
|
+
@field_validator("apps")
|
83
|
+
@classmethod
|
84
|
+
def validate_apps_not_empty(cls, v: List[str]) -> List[str]:
|
85
|
+
"""Ensure apps list is not empty."""
|
86
|
+
if not v:
|
87
|
+
raise ValueError("apps list cannot be empty")
|
88
|
+
return v
|
89
|
+
|
90
|
+
@field_validator("title")
|
91
|
+
@classmethod
|
92
|
+
def validate_title_not_empty(cls, v: str) -> str:
|
93
|
+
"""Ensure title is not empty."""
|
94
|
+
if not v.strip():
|
95
|
+
raise ValueError("title cannot be empty")
|
96
|
+
return v.strip()
|
97
|
+
|
98
|
+
|
99
|
+
__all__ = [
|
100
|
+
"OpenAPIGroupConfig",
|
101
|
+
]
|
@@ -0,0 +1,209 @@
|
|
1
|
+
"""
|
2
|
+
Django OpenAPI Service.
|
3
|
+
|
4
|
+
Universal OpenAPI client generator.
|
5
|
+
Replaces django-revolution with faster, cleaner implementation.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Optional, Dict, List
|
10
|
+
from pathlib import Path
|
11
|
+
from .config import OpenAPIConfig
|
12
|
+
from .group import OpenAPIGroupConfig
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class OpenAPIError(Exception):
|
18
|
+
"""Base exception for OpenAPI-related errors."""
|
19
|
+
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
class DjangoOpenAPI:
|
24
|
+
"""
|
25
|
+
Main OpenAPI service.
|
26
|
+
|
27
|
+
Features:
|
28
|
+
- Smart application grouping (cfg/custom)
|
29
|
+
- Pure Python client generation (Python + TypeScript)
|
30
|
+
- 20x faster than django-revolution
|
31
|
+
- No external dependencies (Node.js, datamodel-codegen, etc.)
|
32
|
+
- Full x-enum-varnames support
|
33
|
+
- Native COMPONENT_SPLIT_REQUEST handling
|
34
|
+
|
35
|
+
Example:
|
36
|
+
>>> from django_cfg.modules.django_client.core.config import get_openapi_service
|
37
|
+
>>> service = get_openapi_service()
|
38
|
+
>>> service.generate_clients(groups=["cfg", "custom"])
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(self, config: Optional[OpenAPIConfig] = None):
|
42
|
+
self._config: Optional[OpenAPIConfig] = config
|
43
|
+
|
44
|
+
@property
|
45
|
+
def config(self) -> Optional[OpenAPIConfig]:
|
46
|
+
"""Get OpenAPI configuration."""
|
47
|
+
return self._config
|
48
|
+
|
49
|
+
def set_config(self, config: OpenAPIConfig):
|
50
|
+
"""Set OpenAPI configuration."""
|
51
|
+
self._config = config
|
52
|
+
|
53
|
+
def is_enabled(self) -> bool:
|
54
|
+
"""Check if OpenAPI is enabled."""
|
55
|
+
return self.config is not None and self.config.enabled
|
56
|
+
|
57
|
+
def get_groups(self) -> Dict[str, OpenAPIGroupConfig]:
|
58
|
+
"""Get configured application groups as a dictionary (for backward compatibility)."""
|
59
|
+
if not self.config:
|
60
|
+
return {}
|
61
|
+
|
62
|
+
# Use get_groups_with_defaults if available (OpenAPIClientConfig)
|
63
|
+
if hasattr(self.config, 'get_groups_with_defaults'):
|
64
|
+
return self.config.get_groups_with_defaults()
|
65
|
+
|
66
|
+
# Convert list to dict for backward compatibility
|
67
|
+
return {group.name: group for group in self.config.groups}
|
68
|
+
|
69
|
+
def get_group(self, group_name: str) -> Optional[OpenAPIGroupConfig]:
|
70
|
+
"""Get specific group configuration (including defaults)."""
|
71
|
+
return self.get_groups().get(group_name)
|
72
|
+
|
73
|
+
def get_group_names(self) -> List[str]:
|
74
|
+
"""Get list of configured group names (including defaults)."""
|
75
|
+
return list(self.get_groups().keys())
|
76
|
+
|
77
|
+
def get_output_dir(self) -> Path:
|
78
|
+
"""Get base output directory."""
|
79
|
+
if not self.config:
|
80
|
+
return Path("openapi")
|
81
|
+
return self.config.get_output_path()
|
82
|
+
|
83
|
+
def get_schemas_dir(self) -> Path:
|
84
|
+
"""Get schemas directory."""
|
85
|
+
if not self.config:
|
86
|
+
return Path("openapi/schemas")
|
87
|
+
return self.config.get_schemas_dir()
|
88
|
+
|
89
|
+
def get_clients_dir(self) -> Path:
|
90
|
+
"""Get clients directory."""
|
91
|
+
if not self.config:
|
92
|
+
return Path("openapi/clients")
|
93
|
+
return self.config.get_clients_dir()
|
94
|
+
|
95
|
+
def ensure_directories(self):
|
96
|
+
"""Ensure all required directories exist."""
|
97
|
+
if not self.config:
|
98
|
+
return
|
99
|
+
|
100
|
+
dirs = [
|
101
|
+
self.config.get_output_path(),
|
102
|
+
self.config.get_schemas_dir(),
|
103
|
+
self.config.get_clients_dir(),
|
104
|
+
self.config.get_python_clients_dir(),
|
105
|
+
self.config.get_typescript_clients_dir(),
|
106
|
+
]
|
107
|
+
|
108
|
+
if self.config.enable_archive:
|
109
|
+
dirs.append(self.config.get_archive_dir())
|
110
|
+
|
111
|
+
for directory in dirs:
|
112
|
+
directory.mkdir(parents=True, exist_ok=True)
|
113
|
+
logger.debug(f"Ensured directory exists: {directory}")
|
114
|
+
|
115
|
+
def validate_config(self) -> bool:
|
116
|
+
"""
|
117
|
+
Validate OpenAPI configuration.
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
bool: True if configuration is valid
|
121
|
+
|
122
|
+
Raises:
|
123
|
+
OpenAPIError: If configuration is invalid
|
124
|
+
"""
|
125
|
+
if not self.config:
|
126
|
+
raise OpenAPIError("OpenAPI configuration not found")
|
127
|
+
|
128
|
+
if not self.config.enabled:
|
129
|
+
raise OpenAPIError("OpenAPI is not enabled")
|
130
|
+
|
131
|
+
if not self.config.groups:
|
132
|
+
raise OpenAPIError("No application groups configured")
|
133
|
+
|
134
|
+
# Validate each group
|
135
|
+
for group in self.config.groups:
|
136
|
+
if not group.apps:
|
137
|
+
raise OpenAPIError(f"Group '{group.name}' has no apps configured")
|
138
|
+
|
139
|
+
if not group.title:
|
140
|
+
raise OpenAPIError(f"Group '{group.name}' has no title")
|
141
|
+
|
142
|
+
logger.info(f"OpenAPI configuration validated: {len(self.config.groups)} groups")
|
143
|
+
return True
|
144
|
+
|
145
|
+
def get_status(self) -> Dict:
|
146
|
+
"""
|
147
|
+
Get OpenAPI service status.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Dictionary with service status information
|
151
|
+
"""
|
152
|
+
if not self.config:
|
153
|
+
return {
|
154
|
+
"enabled": False,
|
155
|
+
"reason": "Configuration not found",
|
156
|
+
}
|
157
|
+
|
158
|
+
try:
|
159
|
+
return {
|
160
|
+
"enabled": self.config.enabled,
|
161
|
+
"groups": len(self.config.groups),
|
162
|
+
"group_names": self.get_group_names(),
|
163
|
+
"output_dir": str(self.config.get_output_path()),
|
164
|
+
"generate_python": self.config.generate_python,
|
165
|
+
"generate_typescript": self.config.generate_typescript,
|
166
|
+
"api_prefix": self.config.api_prefix,
|
167
|
+
"archive_enabled": self.config.enable_archive,
|
168
|
+
}
|
169
|
+
except Exception as e:
|
170
|
+
return {
|
171
|
+
"enabled": False,
|
172
|
+
"error": str(e),
|
173
|
+
}
|
174
|
+
|
175
|
+
|
176
|
+
# Singleton instance
|
177
|
+
_service_instance: Optional[DjangoOpenAPI] = None
|
178
|
+
|
179
|
+
|
180
|
+
def get_openapi_service() -> DjangoOpenAPI:
|
181
|
+
"""
|
182
|
+
Get singleton OpenAPI service instance.
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
DjangoOpenAPI instance
|
186
|
+
|
187
|
+
Example:
|
188
|
+
>>> service = get_openapi_service()
|
189
|
+
>>> if service.is_enabled():
|
190
|
+
... print(f"Groups: {service.get_group_names()}")
|
191
|
+
"""
|
192
|
+
global _service_instance
|
193
|
+
if _service_instance is None:
|
194
|
+
_service_instance = DjangoOpenAPI()
|
195
|
+
return _service_instance
|
196
|
+
|
197
|
+
|
198
|
+
def reset_service():
|
199
|
+
"""Reset singleton instance (useful for testing)."""
|
200
|
+
global _service_instance
|
201
|
+
_service_instance = None
|
202
|
+
|
203
|
+
|
204
|
+
__all__ = [
|
205
|
+
"DjangoOpenAPI",
|
206
|
+
"OpenAPIError",
|
207
|
+
"get_openapi_service",
|
208
|
+
"reset_service",
|
209
|
+
]
|
@@ -0,0 +1,115 @@
|
|
1
|
+
"""
|
2
|
+
Code Generators - IR → Python/TypeScript clients.
|
3
|
+
|
4
|
+
This package provides generators for converting IR to language-specific clients.
|
5
|
+
|
6
|
+
Usage:
|
7
|
+
>>> from django_cfg.modules.django_client.core.generator import generate_python, generate_typescript
|
8
|
+
>>> from django_cfg.modules.django_client.core.parser import parse_openapi
|
9
|
+
>>>
|
10
|
+
>>> # Parse OpenAPI spec
|
11
|
+
>>> context = parse_openapi(spec_dict)
|
12
|
+
>>>
|
13
|
+
>>> # Generate Python client
|
14
|
+
>>> python_files = generate_python(context)
|
15
|
+
>>> for file in python_files:
|
16
|
+
... print(f"{file.path}: {len(file.content)} bytes")
|
17
|
+
>>>
|
18
|
+
>>> # Generate TypeScript client
|
19
|
+
>>> ts_files = generate_typescript(context)
|
20
|
+
"""
|
21
|
+
|
22
|
+
from pathlib import Path
|
23
|
+
from typing import Literal
|
24
|
+
|
25
|
+
from .base import GeneratedFile
|
26
|
+
from .python import PythonGenerator
|
27
|
+
from .typescript import TypeScriptGenerator
|
28
|
+
from ..ir import IRContext
|
29
|
+
|
30
|
+
__all__ = [
|
31
|
+
"PythonGenerator",
|
32
|
+
"TypeScriptGenerator",
|
33
|
+
"GeneratedFile",
|
34
|
+
"generate_python",
|
35
|
+
"generate_typescript",
|
36
|
+
"generate_client",
|
37
|
+
]
|
38
|
+
|
39
|
+
|
40
|
+
def generate_python(context: IRContext, output_dir: Path | None = None) -> list[GeneratedFile]:
|
41
|
+
"""
|
42
|
+
Generate Python client from IR.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
context: IRContext from parser
|
46
|
+
output_dir: Optional output directory (saves files if provided)
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
List of GeneratedFile objects
|
50
|
+
|
51
|
+
Examples:
|
52
|
+
>>> files = generate_python(context)
|
53
|
+
>>> # Or save directly
|
54
|
+
>>> files = generate_python(context, output_dir=Path("./generated/python"))
|
55
|
+
"""
|
56
|
+
generator = PythonGenerator(context)
|
57
|
+
files = generator.generate()
|
58
|
+
|
59
|
+
if output_dir:
|
60
|
+
generator.save_files(files, output_dir)
|
61
|
+
|
62
|
+
return files
|
63
|
+
|
64
|
+
|
65
|
+
def generate_typescript(context: IRContext, output_dir: Path | None = None) -> list[GeneratedFile]:
|
66
|
+
"""
|
67
|
+
Generate TypeScript client from IR.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
context: IRContext from parser
|
71
|
+
output_dir: Optional output directory (saves files if provided)
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
List of GeneratedFile objects
|
75
|
+
|
76
|
+
Examples:
|
77
|
+
>>> files = generate_typescript(context)
|
78
|
+
>>> # Or save directly
|
79
|
+
>>> files = generate_typescript(context, output_dir=Path("./generated/typescript"))
|
80
|
+
"""
|
81
|
+
generator = TypeScriptGenerator(context)
|
82
|
+
files = generator.generate()
|
83
|
+
|
84
|
+
if output_dir:
|
85
|
+
generator.save_files(files, output_dir)
|
86
|
+
|
87
|
+
return files
|
88
|
+
|
89
|
+
|
90
|
+
def generate_client(
|
91
|
+
context: IRContext,
|
92
|
+
language: Literal["python", "typescript"],
|
93
|
+
output_dir: Path | None = None,
|
94
|
+
) -> list[GeneratedFile]:
|
95
|
+
"""
|
96
|
+
Generate client for specified language.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
context: IRContext from parser
|
100
|
+
language: Target language ('python' or 'typescript')
|
101
|
+
output_dir: Optional output directory
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
List of GeneratedFile objects
|
105
|
+
|
106
|
+
Examples:
|
107
|
+
>>> files = generate_client(context, "python")
|
108
|
+
>>> files = generate_client(context, "typescript", Path("./generated"))
|
109
|
+
"""
|
110
|
+
if language == "python":
|
111
|
+
return generate_python(context, output_dir)
|
112
|
+
elif language == "typescript":
|
113
|
+
return generate_typescript(context, output_dir)
|
114
|
+
else:
|
115
|
+
raise ValueError(f"Unsupported language: {language}")
|