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,178 @@
1
+ """
2
+ Application Group Detector.
3
+
4
+ Detects which Django apps belong to which groups, with wildcard support.
5
+ """
6
+
7
+ import fnmatch
8
+ import logging
9
+ from typing import Dict, List, Set
10
+ from ..config import OpenAPIConfig, OpenAPIGroupConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class GroupDetector:
16
+ """
17
+ Detects Django apps for each configured group.
18
+
19
+ Supports wildcard patterns like "django_cfg.*" to match multiple apps.
20
+
21
+ Example:
22
+ >>> config = OpenAPIConfig(
23
+ ... groups={
24
+ ... "cfg": OpenAPIGroupConfig(
25
+ ... apps=["django_cfg.*"],
26
+ ... title="Framework",
27
+ ... ),
28
+ ... "custom": OpenAPIGroupConfig(
29
+ ... apps=["myapp", "anotherapp"],
30
+ ... title="Custom",
31
+ ... ),
32
+ ... },
33
+ ... )
34
+ >>> detector = GroupDetector(config)
35
+ >>> groups = detector.detect_groups(installed_apps)
36
+ >>> print(groups["cfg"]) # ['django_cfg.admin', 'django_cfg.logging', ...]
37
+ """
38
+
39
+ def __init__(self, config: OpenAPIConfig):
40
+ """
41
+ Initialize detector with configuration.
42
+
43
+ Args:
44
+ config: OpenAPI configuration with groups
45
+ """
46
+ self.config = config
47
+
48
+ def detect_groups(self, installed_apps: List[str]) -> Dict[str, List[str]]:
49
+ """
50
+ Detect which apps belong to which groups.
51
+
52
+ Args:
53
+ installed_apps: List of installed Django apps
54
+
55
+ Returns:
56
+ Dictionary mapping group names to lists of app names
57
+
58
+ Example:
59
+ >>> installed_apps = [
60
+ ... "django_cfg.admin",
61
+ ... "django_cfg.logging",
62
+ ... "myapp",
63
+ ... "anotherapp",
64
+ ... ]
65
+ >>> result = detector.detect_groups(installed_apps)
66
+ >>> result["cfg"]
67
+ ['django_cfg.admin', 'django_cfg.logging']
68
+ >>> result["custom"]
69
+ ['myapp', 'anotherapp']
70
+ """
71
+ result = {}
72
+ app_set = set(installed_apps)
73
+
74
+ for group in self.config.groups:
75
+ matched_apps = self._match_apps(group.apps, app_set)
76
+ result[group.name] = sorted(matched_apps)
77
+
78
+ logger.info(
79
+ f"Group '{group.name}': {len(matched_apps)} apps "
80
+ f"matched from {len(group.apps)} patterns"
81
+ )
82
+ logger.debug(f"Group '{group.name}' apps: {matched_apps}")
83
+
84
+ return result
85
+
86
+ def _match_apps(self, patterns: List[str], installed_apps: Set[str]) -> List[str]:
87
+ """
88
+ Match apps using patterns (supports wildcards).
89
+
90
+ Args:
91
+ patterns: List of app patterns (e.g., ["django_cfg.*", "myapp"])
92
+ installed_apps: Set of installed app names
93
+
94
+ Returns:
95
+ List of matched app names
96
+
97
+ Example:
98
+ >>> patterns = ["django_cfg.*", "myapp"]
99
+ >>> installed_apps = {
100
+ ... "django_cfg.admin",
101
+ ... "django_cfg.logging",
102
+ ... "myapp",
103
+ ... "otherapp",
104
+ ... }
105
+ >>> result = detector._match_apps(patterns, installed_apps)
106
+ >>> sorted(result)
107
+ ['django_cfg.admin', 'django_cfg.logging', 'myapp']
108
+ """
109
+ matched = []
110
+
111
+ for pattern in patterns:
112
+ if "*" in pattern or "?" in pattern or "[" in pattern:
113
+ # Wildcard pattern - use fnmatch
114
+ matches = fnmatch.filter(installed_apps, pattern)
115
+ matched.extend(matches)
116
+ logger.debug(f"Pattern '{pattern}' matched {len(matches)} apps")
117
+ else:
118
+ # Exact match
119
+ if pattern in installed_apps:
120
+ matched.append(pattern)
121
+ logger.debug(f"Exact match: '{pattern}'")
122
+ else:
123
+ logger.warning(f"App '{pattern}' not found in INSTALLED_APPS")
124
+
125
+ # Remove duplicates while preserving order
126
+ seen = set()
127
+ result = []
128
+ for app in matched:
129
+ if app not in seen:
130
+ seen.add(app)
131
+ result.append(app)
132
+
133
+ return result
134
+
135
+ def validate_groups(self, installed_apps: List[str]) -> Dict[str, bool]:
136
+ """
137
+ Validate that all groups have at least one matched app.
138
+
139
+ Args:
140
+ installed_apps: List of installed Django apps
141
+
142
+ Returns:
143
+ Dictionary mapping group names to validation status
144
+
145
+ Example:
146
+ >>> validation = detector.validate_groups(installed_apps)
147
+ >>> if not all(validation.values()):
148
+ ... print("Some groups have no apps!")
149
+ """
150
+ groups = self.detect_groups(installed_apps)
151
+ return {name: len(apps) > 0 for name, apps in groups.items()}
152
+
153
+ def get_ungrouped_apps(self, installed_apps: List[str]) -> List[str]:
154
+ """
155
+ Get list of apps that don't belong to any group.
156
+
157
+ Args:
158
+ installed_apps: List of installed Django apps
159
+
160
+ Returns:
161
+ List of ungrouped app names
162
+
163
+ Example:
164
+ >>> ungrouped = detector.get_ungrouped_apps(installed_apps)
165
+ >>> if ungrouped:
166
+ ... print(f"Ungrouped apps: {ungrouped}")
167
+ """
168
+ groups = self.detect_groups(installed_apps)
169
+ grouped_apps = set()
170
+ for apps in groups.values():
171
+ grouped_apps.update(apps)
172
+
173
+ return sorted(set(installed_apps) - grouped_apps)
174
+
175
+
176
+ __all__ = [
177
+ "GroupDetector",
178
+ ]
@@ -0,0 +1,314 @@
1
+ """
2
+ Application Group Manager.
3
+
4
+ Manages application groups and URL pattern generation.
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+ from types import ModuleType
10
+ from typing import Dict, List, Optional
11
+ from ..config import OpenAPIConfig
12
+ from .detector import GroupDetector
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class GroupManager:
18
+ """
19
+ Manages application groups for OpenAPI schema generation.
20
+
21
+ Features:
22
+ - App detection with wildcard matching
23
+ - Dynamic URL configuration generation per group
24
+ - Group validation
25
+
26
+ Example:
27
+ >>> config = OpenAPIConfig(
28
+ ... groups={
29
+ ... "cfg": OpenAPIGroupConfig(
30
+ ... apps=["django_cfg.*"],
31
+ ... title="Framework API",
32
+ ... ),
33
+ ... },
34
+ ... )
35
+ >>> manager = GroupManager(config, installed_apps)
36
+ >>> groups = manager.get_groups()
37
+ >>> print(groups["cfg"]) # ['django_cfg.admin', 'django_cfg.logging', ...]
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ config: OpenAPIConfig,
43
+ installed_apps: Optional[List[str]] = None,
44
+ groups: Optional[Dict[str, 'OpenAPIGroupConfig']] = None,
45
+ ):
46
+ """
47
+ Initialize group manager.
48
+
49
+ Args:
50
+ config: OpenAPI configuration
51
+ installed_apps: List of installed Django apps (auto-detected if None)
52
+ groups: Override groups (if None, uses config.groups)
53
+ """
54
+ self.config = config
55
+ self._override_groups = groups
56
+ self.detector = GroupDetector(config) if not groups else None
57
+
58
+ # Get installed apps
59
+ if installed_apps is None:
60
+ installed_apps = self._get_installed_apps()
61
+
62
+ self.installed_apps = installed_apps
63
+ self._groups_cache: Optional[Dict[str, List[str]]] = None
64
+
65
+ def _get_installed_apps(self) -> List[str]:
66
+ """
67
+ Get list of installed Django apps.
68
+
69
+ Returns:
70
+ List of app names from Django settings
71
+
72
+ Raises:
73
+ RuntimeError: If Django is not configured
74
+ """
75
+ try:
76
+ from django.conf import settings
77
+
78
+ if not settings.configured:
79
+ raise RuntimeError("Django settings not configured")
80
+
81
+ return list(settings.INSTALLED_APPS)
82
+
83
+ except ImportError:
84
+ raise RuntimeError("Django is not installed")
85
+
86
+ def get_groups(self) -> Dict[str, List[str]]:
87
+ """
88
+ Get detected groups.
89
+
90
+ Returns:
91
+ Dictionary mapping group names to app lists
92
+
93
+ Example:
94
+ >>> groups = manager.get_groups()
95
+ >>> print(f"Groups: {list(groups.keys())}")
96
+ >>> print(f"CFG apps: {groups['cfg']}")
97
+ """
98
+ if self._groups_cache is None:
99
+ if self._override_groups:
100
+ # Use override groups - manually detect apps for each group
101
+ self._groups_cache = {}
102
+ for group_name, group_config in self._override_groups.items():
103
+ matched_apps = []
104
+ for app_pattern in group_config.apps:
105
+ if '*' in app_pattern or '?' in app_pattern:
106
+ # Wildcard matching
107
+ import fnmatch
108
+ matched_apps.extend([
109
+ app for app in self.installed_apps
110
+ if fnmatch.fnmatch(app, app_pattern)
111
+ ])
112
+ else:
113
+ # Exact match
114
+ if app_pattern in self.installed_apps:
115
+ matched_apps.append(app_pattern)
116
+ self._groups_cache[group_name] = matched_apps
117
+ else:
118
+ # Use detector
119
+ self._groups_cache = self.detector.detect_groups(self.installed_apps)
120
+
121
+ return self._groups_cache
122
+
123
+ def get_group_apps(self, group_name: str) -> List[str]:
124
+ """
125
+ Get apps for specific group.
126
+
127
+ Args:
128
+ group_name: Name of the group
129
+
130
+ Returns:
131
+ List of app names for the group
132
+
133
+ Example:
134
+ >>> apps = manager.get_group_apps("cfg")
135
+ >>> print(f"CFG group has {len(apps)} apps")
136
+ """
137
+ groups = self.get_groups()
138
+ return groups.get(group_name, [])
139
+
140
+ def validate_all_groups(self) -> bool:
141
+ """
142
+ Validate that all groups have at least one app.
143
+
144
+ Returns:
145
+ True if all groups are valid
146
+
147
+ Raises:
148
+ ValueError: If any group has no apps
149
+
150
+ Example:
151
+ >>> try:
152
+ ... manager.validate_all_groups()
153
+ ... print("All groups valid!")
154
+ ... except ValueError as e:
155
+ ... print(f"Validation error: {e}")
156
+ """
157
+ validation = self.detector.validate_groups(self.installed_apps)
158
+ invalid_groups = [name for name, valid in validation.items() if not valid]
159
+
160
+ if invalid_groups:
161
+ raise ValueError(
162
+ f"Groups with no matched apps: {', '.join(invalid_groups)}"
163
+ )
164
+
165
+ logger.info(f"All {len(validation)} groups validated successfully")
166
+ return True
167
+
168
+ def get_ungrouped_apps(self) -> List[str]:
169
+ """
170
+ Get apps that don't belong to any group.
171
+
172
+ Returns:
173
+ List of ungrouped app names
174
+
175
+ Example:
176
+ >>> ungrouped = manager.get_ungrouped_apps()
177
+ >>> if ungrouped:
178
+ ... print(f"Warning: {len(ungrouped)} apps not in any group")
179
+ """
180
+ return self.detector.get_ungrouped_apps(self.installed_apps)
181
+
182
+ def create_urlconf_module(self, group_name: str) -> ModuleType:
183
+ """
184
+ Create dynamic URL configuration module for a group.
185
+
186
+ This generates a Python module in memory with URL patterns for all apps
187
+ in the group. The module can be used with drf-spectacular to generate
188
+ OpenAPI schemas for specific app groups.
189
+
190
+ Args:
191
+ group_name: Name of the group
192
+
193
+ Returns:
194
+ Dynamic module with URL patterns
195
+
196
+ Example:
197
+ >>> urlconf = manager.create_urlconf_module("cfg")
198
+ >>> # Use with drf-spectacular:
199
+ >>> # SpectacularAPIView.as_view(urlconf=urlconf)
200
+ """
201
+ apps = self.get_group_apps(group_name)
202
+
203
+ if not apps:
204
+ raise ValueError(f"Group '{group_name}' has no apps")
205
+
206
+ # Generate URL patterns
207
+ urlpatterns = []
208
+ from django.apps import apps as django_apps
209
+
210
+ for app_name in apps:
211
+ # Try to include app URLs
212
+ try:
213
+ # Find app config by full name
214
+ app_config = None
215
+ for config in django_apps.get_app_configs():
216
+ if config.name == app_name:
217
+ app_config = config
218
+ break
219
+
220
+ if app_config:
221
+ # Use actual label from AppConfig
222
+ app_label = app_config.label
223
+ urlpatterns.append(f' path("{app_label}/", include("{app_name}.urls")),')
224
+ else:
225
+ logger.debug(f"App '{app_name}' not found in installed apps")
226
+ except Exception as e:
227
+ logger.debug(f"App '{app_name}' skipped: {e}")
228
+ continue
229
+
230
+ # Create module code
231
+ module_code = f'''"""
232
+ Dynamic URL configuration for group: {group_name}
233
+
234
+ Auto-generated by django-client GroupManager.
235
+ """
236
+
237
+ from django.urls import path, include
238
+
239
+ urlpatterns = [
240
+ {chr(10).join(urlpatterns)}
241
+ ]
242
+ '''
243
+
244
+ # Create module
245
+ module_name = f"_django_client_urlconf_{group_name}"
246
+ module = ModuleType(module_name)
247
+ module.__file__ = f"<dynamic: {group_name}>"
248
+
249
+ # Execute code in module namespace
250
+ exec(module_code, module.__dict__)
251
+
252
+ # Add to sys.modules for import resolution
253
+ sys.modules[module_name] = module
254
+
255
+ logger.info(
256
+ f"Created dynamic urlconf for group '{group_name}' with {len(apps)} apps"
257
+ )
258
+
259
+ return module
260
+
261
+ def get_urlconf_name(self, group_name: str) -> str:
262
+ """
263
+ Get URL configuration module name for a group.
264
+
265
+ Args:
266
+ group_name: Name of the group
267
+
268
+ Returns:
269
+ Module name for use in Django settings
270
+
271
+ Example:
272
+ >>> urlconf_name = manager.get_urlconf_name("cfg")
273
+ >>> # Use in settings:
274
+ >>> # ROOT_URLCONF = urlconf_name
275
+ """
276
+ return f"_django_client_urlconf_{group_name}"
277
+
278
+ def get_statistics(self) -> Dict:
279
+ """
280
+ Get grouping statistics.
281
+
282
+ Returns:
283
+ Dictionary with statistics
284
+
285
+ Example:
286
+ >>> stats = manager.get_statistics()
287
+ >>> print(f"Total groups: {stats['total_groups']}")
288
+ >>> print(f"Total apps: {stats['total_apps']}")
289
+ >>> print(f"Ungrouped apps: {stats['ungrouped_apps']}")
290
+ """
291
+ groups = self.get_groups()
292
+ ungrouped = self.get_ungrouped_apps()
293
+
294
+ total_apps_in_groups = sum(len(apps) for apps in groups.values())
295
+
296
+ return {
297
+ "total_groups": len(groups),
298
+ "total_apps": len(self.installed_apps),
299
+ "total_apps_in_groups": total_apps_in_groups,
300
+ "ungrouped_apps": len(ungrouped),
301
+ "groups": {
302
+ name: {
303
+ "apps": len(apps),
304
+ "apps_list": apps,
305
+ }
306
+ for name, apps in groups.items()
307
+ },
308
+ "ungrouped_apps_list": ungrouped,
309
+ }
310
+
311
+
312
+ __all__ = [
313
+ "GroupManager",
314
+ ]
@@ -0,0 +1,57 @@
1
+ """
2
+ IR (Intermediate Representation) - Unified API representation.
3
+
4
+ This package defines the version-agnostic, language-agnostic intermediate
5
+ representation for Django APIs. All parsers (OpenAPI 3.0.3, 3.1.0) normalize
6
+ to this IR, and all generators (Python, TypeScript) consume this IR.
7
+
8
+ Key Features:
9
+ - Request/Response split (UserRequest vs User)
10
+ - x-enum-varnames support (strongly typed enums)
11
+ - Nullable normalization (both 3.0 and 3.1 → single nullable field)
12
+ - Django metadata (COMPONENT_SPLIT_REQUEST validation)
13
+
14
+ Usage:
15
+ >>> from django_cfg.modules.django_client.core.ir import IRContext, IRSchemaObject, IROperationObject
16
+ >>> # Create schemas
17
+ >>> user_schema = IRSchemaObject(name="User", type="object", is_response_model=True)
18
+ >>> user_request = IRSchemaObject(name="UserRequest", type="object", is_request_model=True)
19
+ >>> # Create operation
20
+ >>> operation = IROperationObject(
21
+ ... operation_id="users_create",
22
+ ... http_method="POST",
23
+ ... path="/api/users/",
24
+ ... request_body=IRRequestBodyObject(schema_name="UserRequest"),
25
+ ... responses={201: IRResponseObject(status_code=201, schema_name="User")},
26
+ ... )
27
+ >>> # Create context
28
+ >>> context = IRContext(
29
+ ... openapi_info=OpenAPIInfo(version="3.1.0", title="My API"),
30
+ ... django_metadata=DjangoGlobalMetadata(component_split_request=True),
31
+ ... schemas={"User": user_schema, "UserRequest": user_request},
32
+ ... operations={"users_create": operation},
33
+ ... )
34
+ """
35
+
36
+ from .context import DjangoGlobalMetadata, IRContext, OpenAPIInfo
37
+ from .operation import (
38
+ IROperationObject,
39
+ IRParameterObject,
40
+ IRRequestBodyObject,
41
+ IRResponseObject,
42
+ )
43
+ from .schema import IRSchemaObject
44
+
45
+ __all__ = [
46
+ # Context (root model)
47
+ "IRContext",
48
+ "OpenAPIInfo",
49
+ "DjangoGlobalMetadata",
50
+ # Schema
51
+ "IRSchemaObject",
52
+ # Operation
53
+ "IROperationObject",
54
+ "IRParameterObject",
55
+ "IRRequestBodyObject",
56
+ "IRResponseObject",
57
+ ]