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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/middleware/api_access.py +6 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +2 -1
- django_cfg/apps/payments/middleware/usage_tracking.py +5 -1
- django_cfg/apps/payments/models/managers/api_key_managers.py +0 -1
- django_cfg/apps/payments/models/managers/subscription_managers.py +0 -1
- django_cfg/apps/payments/services/core/balance_service.py +5 -5
- django_cfg/apps/payments/services/core/subscription_service.py +1 -2
- django_cfg/apps/payments/views/api/balances.py +8 -7
- django_cfg/apps/payments/views/api/base.py +10 -6
- django_cfg/apps/payments/views/api/currencies.py +53 -10
- django_cfg/apps/payments/views/api/payments.py +3 -1
- django_cfg/apps/payments/views/api/subscriptions.py +2 -5
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +82 -41
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/dashboard/sections/documentation.py +391 -0
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -265
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +238 -0
- django_cfg/models/django/{revolution.py → revolution_legacy.py} +8 -0
- django_cfg/modules/django_admin/management/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +188 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +767 -0
- django_cfg/modules/django_client/core/generator/python.py +751 -0
- django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
- django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
- django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/typescript.py +872 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/spectacular/__init__.py +9 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +16 -5
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/modules/django_unfold/dashboard.py +1 -1
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/RECORD +192 -71
- django_cfg/management/commands/generate.py +0 -107
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.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
|
+
]
|