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.
Files changed (193) hide show
  1. django_cfg/apps/agents/management/commands/create_agent.py +1 -1
  2. django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
  3. django_cfg/apps/newsletter/serializers.py +40 -3
  4. django_cfg/apps/newsletter/views/campaigns.py +12 -3
  5. django_cfg/apps/newsletter/views/emails.py +14 -3
  6. django_cfg/apps/newsletter/views/subscriptions.py +12 -2
  7. django_cfg/apps/payments/middleware/api_access.py +6 -2
  8. django_cfg/apps/payments/middleware/rate_limiting.py +2 -1
  9. django_cfg/apps/payments/middleware/usage_tracking.py +5 -1
  10. django_cfg/apps/payments/models/managers/api_key_managers.py +0 -1
  11. django_cfg/apps/payments/models/managers/subscription_managers.py +0 -1
  12. django_cfg/apps/payments/services/core/balance_service.py +5 -5
  13. django_cfg/apps/payments/services/core/subscription_service.py +1 -2
  14. django_cfg/apps/payments/views/api/balances.py +8 -7
  15. django_cfg/apps/payments/views/api/base.py +10 -6
  16. django_cfg/apps/payments/views/api/currencies.py +53 -10
  17. django_cfg/apps/payments/views/api/payments.py +3 -1
  18. django_cfg/apps/payments/views/api/subscriptions.py +2 -5
  19. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  20. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  21. django_cfg/apps/payments/views/overview/views.py +2 -1
  22. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  23. django_cfg/apps/urls.py +106 -45
  24. django_cfg/core/base/config_model.py +2 -2
  25. django_cfg/core/constants.py +1 -1
  26. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  27. django_cfg/core/generation/integration_generators/api.py +82 -41
  28. django_cfg/core/integration/display/startup.py +30 -22
  29. django_cfg/core/integration/url_integration.py +15 -16
  30. django_cfg/dashboard/sections/documentation.py +391 -0
  31. django_cfg/management/commands/check_endpoints.py +11 -160
  32. django_cfg/management/commands/check_settings.py +13 -265
  33. django_cfg/management/commands/clear_constance.py +13 -201
  34. django_cfg/management/commands/create_token.py +13 -321
  35. django_cfg/management/commands/generate_clients.py +23 -0
  36. django_cfg/management/commands/list_urls.py +13 -306
  37. django_cfg/management/commands/migrate_all.py +13 -126
  38. django_cfg/management/commands/migrator.py +13 -396
  39. django_cfg/management/commands/rundramatiq.py +15 -247
  40. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  41. django_cfg/management/commands/runserver_ngrok.py +15 -160
  42. django_cfg/management/commands/script.py +12 -488
  43. django_cfg/management/commands/show_config.py +12 -215
  44. django_cfg/management/commands/show_urls.py +12 -342
  45. django_cfg/management/commands/superuser.py +15 -295
  46. django_cfg/management/commands/task_clear.py +14 -217
  47. django_cfg/management/commands/task_status.py +13 -248
  48. django_cfg/management/commands/test_email.py +15 -86
  49. django_cfg/management/commands/test_telegram.py +14 -61
  50. django_cfg/management/commands/test_twilio.py +15 -105
  51. django_cfg/management/commands/tree.py +13 -383
  52. django_cfg/management/commands/validate_openapi.py +10 -0
  53. django_cfg/middleware/README.md +1 -1
  54. django_cfg/middleware/user_activity.py +3 -3
  55. django_cfg/models/__init__.py +2 -2
  56. django_cfg/models/api/drf/spectacular.py +6 -6
  57. django_cfg/models/django/__init__.py +2 -2
  58. django_cfg/models/django/openapi.py +238 -0
  59. django_cfg/models/django/{revolution.py → revolution_legacy.py} +8 -0
  60. django_cfg/modules/django_admin/management/__init__.py +0 -0
  61. django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
  62. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  63. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  64. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  65. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  66. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  67. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  68. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  69. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  70. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  71. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  72. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  73. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  74. django_cfg/modules/django_client/__init__.py +20 -0
  75. django_cfg/modules/django_client/apps.py +35 -0
  76. django_cfg/modules/django_client/core/__init__.py +56 -0
  77. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  78. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  79. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  80. django_cfg/modules/django_client/core/cli/main.py +235 -0
  81. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  82. django_cfg/modules/django_client/core/config/config.py +188 -0
  83. django_cfg/modules/django_client/core/config/group.py +101 -0
  84. django_cfg/modules/django_client/core/config/service.py +209 -0
  85. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  86. django_cfg/modules/django_client/core/generator/base.py +767 -0
  87. django_cfg/modules/django_client/core/generator/python.py +751 -0
  88. django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
  89. django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
  90. django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
  91. django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
  92. django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
  93. django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
  94. django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
  95. django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
  96. django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
  97. django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
  98. django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
  99. django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
  100. django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
  101. django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
  102. django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
  103. django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
  104. django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
  105. django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
  106. django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
  107. django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
  108. django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
  109. django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
  110. django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
  111. django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
  112. django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
  113. django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
  114. django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
  115. django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
  116. django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
  117. django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
  118. django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
  119. django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
  120. django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
  121. django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
  122. django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
  123. django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
  124. django_cfg/modules/django_client/core/generator/typescript.py +872 -0
  125. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  126. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  127. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  128. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  129. django_cfg/modules/django_client/core/ir/context.py +387 -0
  130. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  131. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  132. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  133. django_cfg/modules/django_client/core/parser/base.py +648 -0
  134. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  135. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  136. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  137. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  138. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  139. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  140. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  141. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  142. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  143. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  144. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  145. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  146. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  147. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  148. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  149. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  150. django_cfg/modules/django_client/management/__init__.py +3 -0
  151. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  152. django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
  153. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  154. django_cfg/modules/django_client/spectacular/__init__.py +9 -0
  155. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  156. django_cfg/modules/django_client/urls.py +72 -0
  157. django_cfg/modules/django_email/management/__init__.py +0 -0
  158. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  159. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  160. django_cfg/modules/django_logging/django_logger.py +6 -6
  161. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  162. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  163. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  164. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  165. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  166. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  167. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  168. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  169. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  170. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  171. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  172. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  173. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  174. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  175. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  176. django_cfg/modules/django_unfold/callbacks/main.py +16 -5
  177. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  178. django_cfg/modules/django_unfold/dashboard.py +1 -1
  179. django_cfg/pyproject.toml +2 -6
  180. django_cfg/registry/third_party.py +5 -7
  181. django_cfg/routing/callbacks.py +1 -1
  182. django_cfg/static/admin/css/prose-unfold.css +666 -0
  183. django_cfg/templates/admin/index.html +8 -0
  184. django_cfg/templates/admin/index_new.html +13 -0
  185. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  186. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  187. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  188. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
  189. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/RECORD +192 -71
  190. django_cfg/management/commands/generate.py +0 -107
  191. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
  192. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
  193. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,188 @@
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
+ client_structure: Literal["flat", "namespaced"] = Field(
80
+ default="namespaced",
81
+ description=(
82
+ "Client structure:\n"
83
+ " - flat: All methods in one class (client.posts_list())\n"
84
+ " - namespaced: Organized by tags (client.posts.list())"
85
+ ),
86
+ )
87
+
88
+ # API Configuration
89
+ api_prefix: str = Field(
90
+ default="apix",
91
+ description="API URL prefix (e.g., 'apix' -> /apix/app/endpoint)",
92
+ )
93
+
94
+ # Archive Configuration
95
+ enable_archive: bool = Field(
96
+ default=True,
97
+ description="Enable client archiving with versioning",
98
+ )
99
+
100
+ archive_retention_days: int = Field(
101
+ default=30,
102
+ description="Days to keep archived clients",
103
+ ge=1,
104
+ )
105
+
106
+ # Performance
107
+ max_workers: int = Field(
108
+ default=1,
109
+ description="Number of parallel workers (1 = single-threaded, which is fast enough)",
110
+ ge=1,
111
+ le=20,
112
+ )
113
+
114
+ @field_validator("groups")
115
+ @classmethod
116
+ def validate_groups_when_enabled(cls, v: List[OpenAPIGroupConfig], info) -> List[OpenAPIGroupConfig]:
117
+ """Ensure at least one group is defined when enabled and names are unique."""
118
+ # Access enabled field via info.data
119
+ enabled = info.data.get("enabled", False)
120
+ if enabled and not v:
121
+ raise ValueError("At least one group must be defined when OpenAPI is enabled")
122
+
123
+ # Check for duplicate group names
124
+ names = [group.name for group in v]
125
+ if len(names) != len(set(names)):
126
+ duplicates = [name for name in names if names.count(name) > 1]
127
+ raise ValueError(f"Duplicate group names found: {', '.join(set(duplicates))}")
128
+
129
+ return v
130
+
131
+ @field_validator("api_prefix")
132
+ @classmethod
133
+ def validate_api_prefix(cls, v: str) -> str:
134
+ """Ensure API prefix is valid."""
135
+ v = v.strip().strip("/")
136
+ if not v:
137
+ raise ValueError("api_prefix cannot be empty")
138
+ return v
139
+
140
+ @field_validator("output_dir")
141
+ @classmethod
142
+ def validate_output_dir(cls, v: str) -> str:
143
+ """Ensure output directory is valid."""
144
+ v = v.strip()
145
+ if not v:
146
+ raise ValueError("output_dir cannot be empty")
147
+ return v
148
+
149
+ def get_output_path(self) -> Path:
150
+ """Get absolute output path."""
151
+ return Path(self.output_dir).resolve()
152
+
153
+ def get_schemas_dir(self) -> Path:
154
+ """Get schemas directory path."""
155
+ return self.get_output_path() / "schemas"
156
+
157
+ def get_clients_dir(self) -> Path:
158
+ """Get clients directory path."""
159
+ return self.get_output_path() / "clients"
160
+
161
+ def get_python_clients_dir(self) -> Path:
162
+ """Get Python clients directory path."""
163
+ return self.get_clients_dir() / "python"
164
+
165
+ def get_typescript_clients_dir(self) -> Path:
166
+ """Get TypeScript clients directory path."""
167
+ return self.get_clients_dir() / "typescript"
168
+
169
+ def get_archive_dir(self) -> Path:
170
+ """Get archive directory path."""
171
+ return self.get_output_path() / "archive"
172
+
173
+ def get_group_schema_path(self, group_name: str) -> Path:
174
+ """Get OpenAPI schema path for a group."""
175
+ return self.get_schemas_dir() / f"{group_name}.yaml"
176
+
177
+ def get_group_python_dir(self, group_name: str) -> Path:
178
+ """Get Python client directory for a group."""
179
+ return self.get_python_clients_dir() / group_name
180
+
181
+ def get_group_typescript_dir(self, group_name: str) -> Path:
182
+ """Get TypeScript client directory for a group."""
183
+ return self.get_typescript_clients_dir() / group_name
184
+
185
+
186
+ __all__ = [
187
+ "OpenAPIConfig",
188
+ ]
@@ -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 (ExtendedOpenAPIConfig)
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 django_cfg.modules.django_client.core.generator.base import GeneratedFile
26
+ from django_cfg.modules.django_client.core.generator.python import PythonGenerator
27
+ from django_cfg.modules.django_client.core.generator.typescript import TypeScriptGenerator
28
+ from django_cfg.modules.django_client.core.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}")