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.
Files changed (225) 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/views/api/currencies.py +49 -6
  8. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  9. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  10. django_cfg/apps/payments/views/overview/views.py +2 -1
  11. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  12. django_cfg/apps/urls.py +106 -45
  13. django_cfg/core/base/config_model.py +2 -2
  14. django_cfg/core/constants.py +1 -1
  15. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  16. django_cfg/core/generation/integration_generators/api.py +73 -49
  17. django_cfg/core/integration/display/startup.py +30 -22
  18. django_cfg/core/integration/url_integration.py +15 -16
  19. django_cfg/management/commands/check_endpoints.py +11 -160
  20. django_cfg/management/commands/check_settings.py +13 -348
  21. django_cfg/management/commands/clear_constance.py +13 -201
  22. django_cfg/management/commands/create_token.py +13 -321
  23. django_cfg/management/commands/generate_clients.py +23 -0
  24. django_cfg/management/commands/list_urls.py +13 -306
  25. django_cfg/management/commands/migrate_all.py +13 -126
  26. django_cfg/management/commands/migrator.py +13 -396
  27. django_cfg/management/commands/rundramatiq.py +15 -247
  28. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  29. django_cfg/management/commands/runserver_ngrok.py +15 -160
  30. django_cfg/management/commands/script.py +12 -488
  31. django_cfg/management/commands/show_config.py +12 -215
  32. django_cfg/management/commands/show_urls.py +12 -342
  33. django_cfg/management/commands/superuser.py +15 -295
  34. django_cfg/management/commands/task_clear.py +14 -217
  35. django_cfg/management/commands/task_status.py +13 -248
  36. django_cfg/management/commands/test_email.py +15 -86
  37. django_cfg/management/commands/test_telegram.py +14 -61
  38. django_cfg/management/commands/test_twilio.py +15 -105
  39. django_cfg/management/commands/tree.py +13 -383
  40. django_cfg/management/commands/validate_openapi.py +10 -0
  41. django_cfg/middleware/README.md +1 -1
  42. django_cfg/middleware/user_activity.py +3 -3
  43. django_cfg/models/__init__.py +2 -2
  44. django_cfg/models/api/drf/spectacular.py +6 -6
  45. django_cfg/models/django/__init__.py +2 -2
  46. django_cfg/models/django/openapi.py +162 -0
  47. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  48. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  49. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  50. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  51. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  52. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  53. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  54. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  55. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  56. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  57. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  58. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  59. django_cfg/modules/django_client/__init__.py +20 -0
  60. django_cfg/modules/django_client/apps.py +35 -0
  61. django_cfg/modules/django_client/core/__init__.py +56 -0
  62. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  63. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  64. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  65. django_cfg/modules/django_client/core/cli/main.py +235 -0
  66. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  67. django_cfg/modules/django_client/core/config/config.py +208 -0
  68. django_cfg/modules/django_client/core/config/group.py +101 -0
  69. django_cfg/modules/django_client/core/config/service.py +209 -0
  70. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  71. django_cfg/modules/django_client/core/generator/base.py +838 -0
  72. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  73. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  74. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  75. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  76. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  77. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  78. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  79. django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
  80. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
  81. django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
  82. django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
  83. django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
  84. django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
  85. django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
  86. django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
  87. django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
  88. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  89. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  90. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  91. django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
  92. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
  93. django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
  94. django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
  95. django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
  96. django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
  97. django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
  98. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  99. django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
  100. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  101. django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
  102. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  103. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  104. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  105. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  106. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  107. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
  108. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  109. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  110. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  111. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  112. django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
  113. django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
  114. django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
  115. django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
  116. django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
  117. django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
  118. django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
  119. django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
  120. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  121. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  122. django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
  123. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
  124. django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
  125. django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
  126. django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
  127. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  128. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  129. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  130. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  131. django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
  132. django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
  133. django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
  134. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  135. django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
  136. django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -0
  137. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  138. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  139. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  140. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  141. django_cfg/modules/django_client/core/ir/context.py +387 -0
  142. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  143. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  144. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  145. django_cfg/modules/django_client/core/parser/base.py +648 -0
  146. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  147. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  148. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  149. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  150. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  151. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  152. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  153. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  154. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  155. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  156. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  157. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  158. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  159. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  160. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  161. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  162. django_cfg/modules/django_client/management/__init__.py +3 -0
  163. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  164. django_cfg/modules/django_client/management/commands/generate_client.py +427 -0
  165. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  166. django_cfg/modules/django_client/pytest.ini +30 -0
  167. django_cfg/modules/django_client/spectacular/__init__.py +10 -0
  168. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  169. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  170. django_cfg/modules/django_client/urls.py +72 -0
  171. django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
  172. django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
  173. django_cfg/modules/django_dashboard/management/__init__.py +0 -0
  174. django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
  175. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  176. django_cfg/modules/django_dashboard/sections/documentation.py +391 -0
  177. django_cfg/modules/django_email/management/__init__.py +0 -0
  178. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  179. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  180. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  181. django_cfg/modules/django_logging/django_logger.py +6 -6
  182. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  183. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  184. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  185. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  186. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  187. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  188. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  189. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  190. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  191. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  192. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  193. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  194. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  195. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  196. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  197. django_cfg/modules/django_unfold/callbacks/main.py +21 -10
  198. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  199. django_cfg/pyproject.toml +2 -6
  200. django_cfg/registry/third_party.py +5 -7
  201. django_cfg/routing/callbacks.py +1 -1
  202. django_cfg/static/admin/css/prose-unfold.css +666 -0
  203. django_cfg/templates/admin/index.html +8 -0
  204. django_cfg/templates/admin/index_new.html +13 -0
  205. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  206. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  207. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  208. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/METADATA +2 -2
  209. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
  210. django_cfg/management/commands/generate.py +0 -107
  211. /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
  212. /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
  213. /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
  214. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  215. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  216. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  217. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  218. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  219. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  220. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  221. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  222. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  223. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
  224. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
  225. {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}")