django-cfg 1.4.15__py3-none-any.whl → 1.4.17__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/leads/urls.py +2 -1
- django_cfg/apps/payments/urls.py +4 -4
- django_cfg/core/base/config_model.py +7 -7
- django_cfg/core/generation/core_generators/settings.py +2 -3
- django_cfg/core/generation/core_generators/static.py +9 -9
- django_cfg/core/generation/integration_generators/api.py +1 -1
- django_cfg/models/infrastructure/security.py +33 -2
- django_cfg/modules/django_client/core/generator/base.py +18 -20
- django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +56 -113
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +102 -232
- django_cfg/modules/django_client/core/generator/typescript/naming.py +83 -0
- django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +8 -4
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +25 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +30 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/index.ts.jinja +29 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/mutation_hook.ts.jinja +25 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/query_hook.ts.jinja +21 -0
- django_cfg/modules/django_client/core/groups/manager.py +51 -26
- django_cfg/modules/django_client/spectacular/__init__.py +3 -2
- django_cfg/modules/django_client/spectacular/schema.py +50 -0
- django_cfg/pyproject.toml +1 -1
- {django_cfg-1.4.15.dist-info → django_cfg-1.4.17.dist-info}/METADATA +1 -1
- {django_cfg-1.4.15.dist-info → django_cfg-1.4.17.dist-info}/RECORD +26 -19
- {django_cfg-1.4.15.dist-info → django_cfg-1.4.17.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.15.dist-info → django_cfg-1.4.17.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.15.dist-info → django_cfg-1.4.17.dist-info}/licenses/LICENSE +0 -0
django_cfg/apps/leads/urls.py
CHANGED
@@ -8,8 +8,9 @@ from rest_framework.routers import DefaultRouter
|
|
8
8
|
from .views import LeadViewSet
|
9
9
|
|
10
10
|
# Create router
|
11
|
+
# Note: Empty prefix because URL is already under cfg/leads/
|
11
12
|
router = DefaultRouter()
|
12
|
-
router.register(r"
|
13
|
+
router.register(r"", LeadViewSet, basename="lead")
|
13
14
|
|
14
15
|
app_name = "cfg_leads"
|
15
16
|
|
django_cfg/apps/payments/urls.py
CHANGED
@@ -22,7 +22,7 @@ app_name = 'cfg_payments'
|
|
22
22
|
|
23
23
|
# Main router for global endpoints
|
24
24
|
router = DefaultRouter()
|
25
|
-
router.register(r'
|
25
|
+
router.register(r'payment', PaymentViewSet, basename='payment')
|
26
26
|
router.register(r'balances', UserBalanceViewSet, basename='balance')
|
27
27
|
router.register(r'transactions', TransactionViewSet, basename='transaction')
|
28
28
|
router.register(r'currencies', CurrencyViewSet, basename='currency')
|
@@ -38,7 +38,7 @@ users_router = routers.SimpleRouter()
|
|
38
38
|
users_router.register(r'users', UserPaymentViewSet, basename='user') # Base for nesting
|
39
39
|
|
40
40
|
payments_router = routers.NestedSimpleRouter(users_router, r'users', lookup='user')
|
41
|
-
payments_router.register(r'
|
41
|
+
payments_router.register(r'payment', UserPaymentViewSet, basename='user-payment')
|
42
42
|
|
43
43
|
subscriptions_router = routers.NestedSimpleRouter(users_router, r'users', lookup='user')
|
44
44
|
subscriptions_router.register(r'subscriptions', UserSubscriptionViewSet, basename='user-subscription')
|
@@ -57,8 +57,8 @@ urlpatterns = [
|
|
57
57
|
path('', include(apikeys_router.urls)),
|
58
58
|
|
59
59
|
# Custom API endpoints
|
60
|
-
path('
|
61
|
-
path('
|
60
|
+
path('payment/create/', PaymentCreateView.as_view(), name='payment-create'),
|
61
|
+
path('payment/status/<uuid:pk>/', PaymentStatusView.as_view(), name='payment-status'),
|
62
62
|
|
63
63
|
# Note: currencies/convert/ is handled by CurrencyViewSet action
|
64
64
|
# path('currencies/convert/', CurrencyConversionView.as_view(), name='currency-convert'),
|
@@ -483,14 +483,14 @@ class DjangoConfig(BaseModel):
|
|
483
483
|
"""
|
484
484
|
Get the base directory of the project.
|
485
485
|
|
486
|
-
Looks for manage.py
|
486
|
+
Looks for manage.py starting from current working directory and going up.
|
487
487
|
Falls back to current working directory if not found.
|
488
|
+
|
489
|
+
This ensures we find the Django project root, not the django-cfg package location.
|
488
490
|
"""
|
489
491
|
if self._base_dir is None:
|
490
|
-
|
491
|
-
|
492
|
-
# Start from current working directory
|
493
|
-
current_path = Path(os.path.dirname(os.path.abspath(__file__)))
|
492
|
+
# Start from current working directory (where Django runs)
|
493
|
+
current_path = Path.cwd().resolve()
|
494
494
|
|
495
495
|
# Look for manage.py in current directory and parents
|
496
496
|
for path in [current_path] + list(current_path.parents):
|
@@ -499,9 +499,9 @@ class DjangoConfig(BaseModel):
|
|
499
499
|
self._base_dir = path
|
500
500
|
break
|
501
501
|
|
502
|
-
# If still not found, use current directory
|
502
|
+
# If still not found, use current working directory
|
503
503
|
if self._base_dir is None:
|
504
|
-
self._base_dir =
|
504
|
+
self._base_dir = current_path
|
505
505
|
|
506
506
|
return self._base_dir
|
507
507
|
|
@@ -77,9 +77,8 @@ class CoreSettingsGenerator:
|
|
77
77
|
# Auto-use django-cfg accounts CustomUser if accounts is enabled
|
78
78
|
settings["AUTH_USER_MODEL"] = "django_cfg_accounts.CustomUser"
|
79
79
|
|
80
|
-
# Add base directory
|
81
|
-
|
82
|
-
settings["BASE_DIR"] = self.config._base_dir
|
80
|
+
# Add base directory (always set, auto-detects from manage.py location)
|
81
|
+
settings["BASE_DIR"] = self.config.base_dir
|
83
82
|
|
84
83
|
# Add default auto field
|
85
84
|
settings["DEFAULT_AUTO_FIELD"] = "django.db.models.BigAutoField"
|
@@ -60,15 +60,15 @@ class StaticFilesGenerator:
|
|
60
60
|
"WHITENOISE_MAX_AGE": 0 if self.config.debug else 3600, # No cache in debug, 1 hour in prod
|
61
61
|
}
|
62
62
|
|
63
|
-
# Set paths relative to base directory
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
63
|
+
# Set paths relative to base directory (always set, auto-detects from manage.py)
|
64
|
+
base_dir = self.config.base_dir
|
65
|
+
settings.update({
|
66
|
+
"STATIC_ROOT": base_dir / "staticfiles",
|
67
|
+
"MEDIA_ROOT": base_dir / "media",
|
68
|
+
"STATICFILES_DIRS": [
|
69
|
+
base_dir / "static",
|
70
|
+
],
|
71
|
+
})
|
72
72
|
|
73
73
|
# Static files finders
|
74
74
|
settings["STATICFILES_FINDERS"] = [
|
@@ -112,7 +112,7 @@ class APIFrameworksGenerator:
|
|
112
112
|
|
113
113
|
# Build REST_FRAMEWORK settings
|
114
114
|
rest_framework = {
|
115
|
-
"DEFAULT_SCHEMA_CLASS": "
|
115
|
+
"DEFAULT_SCHEMA_CLASS": "django_cfg.modules.django_client.spectacular.schema.PathBasedAutoSchema",
|
116
116
|
"DEFAULT_PAGINATION_CLASS": "django_cfg.middleware.pagination.DefaultPagination",
|
117
117
|
"PAGE_SIZE": 100,
|
118
118
|
"DEFAULT_RENDERER_CLASSES": [
|
@@ -80,11 +80,42 @@ class SecurityConfig(BaseConfig):
|
|
80
80
|
self.session_cookie_secure = True
|
81
81
|
|
82
82
|
def configure_for_development(self) -> None:
|
83
|
-
"""
|
83
|
+
"""
|
84
|
+
Configure security settings for development.
|
85
|
+
|
86
|
+
In development:
|
87
|
+
- CORS allows all origins for convenience
|
88
|
+
- CSRF requires explicit trusted origins (CORS setting doesn't affect CSRF)
|
89
|
+
- Adds common dev ports + existing csrf_trusted_origins
|
90
|
+
"""
|
84
91
|
self.cors_allow_all_origins = True
|
85
92
|
self.cors_allowed_origins = []
|
86
93
|
self.csrf_cookie_secure = False
|
87
|
-
|
94
|
+
|
95
|
+
# Common development origins for CSRF
|
96
|
+
# Note: CORS_ALLOW_ALL_ORIGINS doesn't affect CSRF - it needs explicit origins
|
97
|
+
|
98
|
+
# function smart diapason for dev ports
|
99
|
+
def smart_diapason(start: int, end: int) -> List[str]:
|
100
|
+
return [f'http://localhost:{i}' for i in range(start, end)]
|
101
|
+
|
102
|
+
dev_local_origins = [
|
103
|
+
'http://localhost:3000',
|
104
|
+
'http://localhost:8000',
|
105
|
+
'http://127.0.0.1:3000',
|
106
|
+
'http://127.0.0.1:8000',
|
107
|
+
] + smart_diapason(3000, 3010) + smart_diapason(8000, 8010)
|
108
|
+
|
109
|
+
# Combine dev defaults with existing trusted origins (from security_domains)
|
110
|
+
# Remove duplicates while preserving order
|
111
|
+
combined = dev_local_origins + self.csrf_trusted_origins
|
112
|
+
|
113
|
+
# unique and sorted
|
114
|
+
combined_unique = list(dict.fromkeys(combined))
|
115
|
+
combined_unique.sort()
|
116
|
+
|
117
|
+
self.csrf_trusted_origins = combined_unique
|
118
|
+
|
88
119
|
self.ssl_redirect = False
|
89
120
|
self.hsts_enabled = False
|
90
121
|
self.session_cookie_secure = False
|
@@ -208,7 +208,7 @@ class BaseGenerator(ABC):
|
|
208
208
|
Extract Django app name from URL path.
|
209
209
|
|
210
210
|
Args:
|
211
|
-
path: URL path (e.g., "/django_cfg_leads/leads/", "/django_cfg_newsletter/campaigns/")
|
211
|
+
path: URL path (e.g., "/django_cfg_leads/leads/", "/django_cfg_newsletter/campaigns/", "/cfg/accounts/otp/")
|
212
212
|
|
213
213
|
Returns:
|
214
214
|
App name without trailing slash, or None if no app detected
|
@@ -220,10 +220,16 @@ class BaseGenerator(ABC):
|
|
220
220
|
'django_cfg_newsletter'
|
221
221
|
>>> generator.extract_app_from_path("/api/users/")
|
222
222
|
'api'
|
223
|
+
>>> generator.extract_app_from_path("/cfg/accounts/otp/")
|
224
|
+
'accounts'
|
223
225
|
"""
|
224
226
|
# Remove leading/trailing slashes and split
|
225
227
|
parts = path.strip('/').split('/')
|
226
228
|
|
229
|
+
# For cfg group URLs (/cfg/accounts/, /cfg/support/), skip the 'cfg' prefix
|
230
|
+
if len(parts) >= 2 and parts[0] == 'cfg':
|
231
|
+
return parts[1]
|
232
|
+
|
227
233
|
# First part is usually the app name
|
228
234
|
if parts:
|
229
235
|
return parts[0]
|
@@ -396,26 +402,15 @@ class BaseGenerator(ABC):
|
|
396
402
|
'retrieve' # No prefix to remove
|
397
403
|
"""
|
398
404
|
from django.utils.text import slugify
|
405
|
+
import re
|
399
406
|
|
400
407
|
# First, strip common app label prefixes from operation_id
|
401
|
-
# This handles cases like "django_cfg_newsletter_campaigns_list"
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
'django_cfg_support_',
|
408
|
-
'django_cfg_agents_',
|
409
|
-
'django_cfg_knowbase_',
|
410
|
-
'django_cfg_payments_',
|
411
|
-
'django_cfg_tasks_',
|
412
|
-
'django_cfg_', # Catch-all for any django_cfg_* prefixes
|
413
|
-
]
|
414
|
-
|
415
|
-
for app_prefix in app_prefixes_to_strip:
|
416
|
-
if cleaned_op_id.startswith(app_prefix):
|
417
|
-
cleaned_op_id = cleaned_op_id[len(app_prefix):]
|
418
|
-
break
|
408
|
+
# This handles cases like "django_cfg_newsletter_campaigns_list" or "cfg_support_tickets_list"
|
409
|
+
# Remove only the cfg/django_cfg prefix, not the entire app name
|
410
|
+
# Examples:
|
411
|
+
# cfg_support_tickets_list → support_tickets_list
|
412
|
+
# django_cfg_accounts_otp_request → accounts_otp_request
|
413
|
+
cleaned_op_id = re.sub(r'^(django_)?cfg_', '', operation_id)
|
419
414
|
|
420
415
|
# Now try to remove the normalized tag as a prefix
|
421
416
|
# Normalize tag same way as tag_to_property_name but without adding group prefix
|
@@ -432,7 +427,10 @@ class BaseGenerator(ABC):
|
|
432
427
|
# Strip leading underscores from tag
|
433
428
|
normalized_tag = normalized_tag.lstrip('_')
|
434
429
|
|
435
|
-
#
|
430
|
+
# Remove tag prefix from operation_id if it matches
|
431
|
+
# This ensures methods in each API folder have clean, contextual names
|
432
|
+
# e.g., in cfg__support: "support_tickets_list" → "tickets_list"
|
433
|
+
# e.g., in cfg__accounts: "accounts_otp_request" → "otp_request"
|
436
434
|
tag_prefix = f"{normalized_tag}_"
|
437
435
|
if cleaned_op_id.startswith(tag_prefix):
|
438
436
|
cleaned_op_id = cleaned_op_id[len(tag_prefix):]
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
13
13
|
from jinja2 import Environment
|
14
14
|
from ..base import GeneratedFile, BaseGenerator
|
15
15
|
from ...ir import IRContext, IROperationObject
|
16
|
+
from .naming import operation_to_method_name
|
16
17
|
|
17
18
|
|
18
19
|
class FetchersGenerator:
|
@@ -33,7 +34,7 @@ class FetchersGenerator:
|
|
33
34
|
|
34
35
|
def generate_fetcher_function(self, operation: IROperationObject) -> str:
|
35
36
|
"""
|
36
|
-
Generate a single fetcher function for an operation.
|
37
|
+
Generate a single fetcher function for an operation using Jinja2 template.
|
37
38
|
|
38
39
|
Args:
|
39
40
|
operation: IROperationObject to convert to fetcher
|
@@ -59,99 +60,60 @@ class FetchersGenerator:
|
|
59
60
|
|
60
61
|
# Get API client call
|
61
62
|
api_call = self._get_api_call(operation)
|
62
|
-
|
63
|
-
# Build JSDoc comment
|
64
|
-
jsdoc = self._generate_jsdoc(operation, func_name)
|
65
|
-
|
66
|
-
# Build function
|
67
|
-
lines = []
|
68
|
-
|
69
|
-
# JSDoc
|
70
|
-
if jsdoc:
|
71
|
-
lines.append(jsdoc)
|
72
|
-
|
73
|
-
# Function signature with optional client parameter
|
74
|
-
if param_info['func_params']:
|
75
|
-
lines.append(f"export async function {func_name}(")
|
76
|
-
lines.append(f" {param_info['func_params']},")
|
77
|
-
lines.append(f" client?: API")
|
78
|
-
lines.append(f"): Promise<{response_type}> {{")
|
79
|
-
else:
|
80
|
-
lines.append(f"export async function {func_name}(")
|
81
|
-
lines.append(f" client?: API")
|
82
|
-
lines.append(f"): Promise<{response_type}> {{")
|
83
|
-
|
84
|
-
# Get client instance (either passed or global)
|
85
|
-
lines.append(" const api = client || getAPIInstance()")
|
86
|
-
lines.append("")
|
87
|
-
|
88
|
-
# Function body - build API call
|
89
|
-
api_call_params = param_info['api_call_params']
|
90
|
-
# Replace API. with api.
|
63
|
+
# Replace API. with api. for instance method
|
91
64
|
api_call_instance = api_call.replace("API.", "api.")
|
92
65
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
lines.append("}")
|
105
|
-
|
106
|
-
return "\n".join(lines)
|
66
|
+
# Render template
|
67
|
+
template = self.jinja_env.get_template('fetchers/function.ts.jinja')
|
68
|
+
return template.render(
|
69
|
+
operation=operation,
|
70
|
+
func_name=func_name,
|
71
|
+
func_params=param_info['func_params'],
|
72
|
+
response_type=response_type,
|
73
|
+
response_schema=response_schema,
|
74
|
+
api_call=api_call_instance,
|
75
|
+
api_call_params=param_info['api_call_params']
|
76
|
+
)
|
107
77
|
|
108
78
|
def _operation_to_function_name(self, operation: IROperationObject) -> str:
|
109
79
|
"""
|
110
80
|
Convert operation to function name.
|
111
|
-
|
81
|
+
|
82
|
+
Fetchers are organized into tag-specific files but also exported globally,
|
83
|
+
so we include the tag in the name to avoid collisions.
|
84
|
+
|
112
85
|
Examples:
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
users_partial_update (PATCH) -> partialUpdateUsers
|
118
|
-
users_destroy (DELETE) -> deleteUsers
|
86
|
+
cfg_support_tickets_list -> getSupportTicketsList
|
87
|
+
cfg_health_drf_retrieve -> getHealthDrf
|
88
|
+
cfg_accounts_otp_request_create -> createAccountsOtpRequest
|
89
|
+
cfg_accounts_profile_partial_update (PUT) -> partialUpdateAccountsProfilePut
|
119
90
|
"""
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
elif
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
elif
|
141
|
-
|
142
|
-
return f"delete{self._to_pascal_case(resource)}"
|
91
|
+
|
92
|
+
|
93
|
+
# Remove cfg_ prefix but keep tag + resource for uniqueness
|
94
|
+
operation_id = operation.operation_id
|
95
|
+
# Remove only cfg_/django_cfg_ prefix
|
96
|
+
if operation_id.startswith('django_cfg_'):
|
97
|
+
operation_id = operation_id.replace('django_cfg_', '', 1)
|
98
|
+
elif operation_id.startswith('cfg_'):
|
99
|
+
operation_id = operation_id.replace('cfg_', '', 1)
|
100
|
+
|
101
|
+
# Determine prefix based on HTTP method
|
102
|
+
if operation.http_method == 'GET':
|
103
|
+
prefix = 'get'
|
104
|
+
elif operation.http_method == 'POST':
|
105
|
+
prefix = 'create'
|
106
|
+
elif operation.http_method in ('PUT', 'PATCH'):
|
107
|
+
if '_partial_update' in operation_id:
|
108
|
+
prefix = 'partialUpdate'
|
109
|
+
else:
|
110
|
+
prefix = 'update'
|
111
|
+
elif operation.http_method == 'DELETE':
|
112
|
+
prefix = 'delete'
|
143
113
|
else:
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
def _to_pascal_case(self, snake_str: str) -> str:
|
148
|
-
"""Convert snake_case to PascalCase."""
|
149
|
-
return ''.join(word.capitalize() for word in snake_str.split('_'))
|
150
|
-
|
151
|
-
def _to_camel_case(self, snake_str: str) -> str:
|
152
|
-
"""Convert snake_case to camelCase."""
|
153
|
-
components = snake_str.split('_')
|
154
|
-
return components[0] + ''.join(x.capitalize() for x in components[1:])
|
114
|
+
prefix = ''
|
115
|
+
|
116
|
+
return operation_to_method_name(operation_id, operation.http_method, prefix, self.base)
|
155
117
|
|
156
118
|
def _get_param_structure(self, operation: IROperationObject) -> dict:
|
157
119
|
"""
|
@@ -321,46 +283,27 @@ class FetchersGenerator:
|
|
321
283
|
def _get_api_call(self, operation: IROperationObject) -> str:
|
322
284
|
"""
|
323
285
|
Get API client method call path.
|
286
|
+
|
287
|
+
Must match the naming logic in operations_generator to ensure correct method calls.
|
324
288
|
|
325
289
|
Examples:
|
326
290
|
API.users.list
|
327
291
|
API.users.retrieve
|
328
292
|
API.posts.create
|
293
|
+
API.accounts.otpRequest (custom action)
|
329
294
|
"""
|
330
|
-
|
295
|
+
|
296
|
+
|
331
297
|
tag = operation.tags[0] if operation.tags else "default"
|
332
298
|
tag_property = self.base.tag_to_property_name(tag)
|
333
299
|
|
334
|
-
# Get method name
|
335
|
-
|
336
|
-
|
300
|
+
# Get method name using same logic as client generation (empty prefix)
|
301
|
+
operation_id = self.base.remove_tag_prefix(operation.operation_id, tag)
|
302
|
+
# Pass path to distinguish custom actions
|
303
|
+
method_name = operation_to_method_name(operation_id, operation.http_method, '', self.base, operation.path)
|
337
304
|
|
338
305
|
return f"API.{tag_property}.{method_name}"
|
339
306
|
|
340
|
-
def _generate_jsdoc(self, operation: IROperationObject, func_name: str) -> str:
|
341
|
-
"""Generate JSDoc comment for function."""
|
342
|
-
lines = ["/**"]
|
343
|
-
|
344
|
-
# Summary
|
345
|
-
if operation.summary:
|
346
|
-
lines.append(f" * {operation.summary}")
|
347
|
-
else:
|
348
|
-
lines.append(f" * {func_name}")
|
349
|
-
|
350
|
-
# Description
|
351
|
-
if operation.description:
|
352
|
-
lines.append(" *")
|
353
|
-
for desc_line in operation.description.split("\n"):
|
354
|
-
lines.append(f" * {desc_line}")
|
355
|
-
|
356
|
-
# HTTP method and path
|
357
|
-
lines.append(" *")
|
358
|
-
lines.append(f" * @method {operation.http_method}")
|
359
|
-
lines.append(f" * @path {operation.path}")
|
360
|
-
|
361
|
-
lines.append(" */")
|
362
|
-
return "\n".join(lines)
|
363
|
-
|
364
307
|
def generate_tag_fetchers_file(
|
365
308
|
self,
|
366
309
|
tag: str,
|