django-cfg 1.4.15__py3-none-any.whl → 1.4.19__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 (29) hide show
  1. django_cfg/apps/leads/urls.py +2 -1
  2. django_cfg/apps/payments/urls.py +4 -4
  3. django_cfg/core/base/config_model.py +7 -7
  4. django_cfg/core/generation/core_generators/settings.py +2 -3
  5. django_cfg/core/generation/core_generators/static.py +9 -9
  6. django_cfg/core/generation/integration_generators/api.py +1 -1
  7. django_cfg/models/infrastructure/security.py +33 -2
  8. django_cfg/modules/django_client/core/generator/base.py +18 -20
  9. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +56 -113
  10. django_cfg/modules/django_client/core/generator/typescript/generator.py +1 -1
  11. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +102 -232
  12. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +9 -4
  13. django_cfg/modules/django_client/core/generator/typescript/naming.py +83 -0
  14. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +19 -7
  15. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +40 -33
  16. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +25 -0
  17. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +31 -0
  18. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/index.ts.jinja +29 -0
  19. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/mutation_hook.ts.jinja +25 -0
  20. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/query_hook.ts.jinja +20 -0
  21. django_cfg/modules/django_client/core/groups/manager.py +51 -26
  22. django_cfg/modules/django_client/spectacular/__init__.py +3 -2
  23. django_cfg/modules/django_client/spectacular/schema.py +50 -0
  24. django_cfg/pyproject.toml +1 -1
  25. {django_cfg-1.4.15.dist-info → django_cfg-1.4.19.dist-info}/METADATA +1 -1
  26. {django_cfg-1.4.15.dist-info → django_cfg-1.4.19.dist-info}/RECORD +29 -22
  27. {django_cfg-1.4.15.dist-info → django_cfg-1.4.19.dist-info}/WHEEL +0 -0
  28. {django_cfg-1.4.15.dist-info → django_cfg-1.4.19.dist-info}/entry_points.txt +0 -0
  29. {django_cfg-1.4.15.dist-info → django_cfg-1.4.19.dist-info}/licenses/LICENSE +0 -0
@@ -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"leads", LeadViewSet, basename="lead")
13
+ router.register(r"", LeadViewSet, basename="lead")
13
14
 
14
15
  app_name = "cfg_leads"
15
16
 
@@ -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'payments', PaymentViewSet, basename='payment')
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'payments', UserPaymentViewSet, basename='user-payment')
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('payments/create/', PaymentCreateView.as_view(), name='payment-create'),
61
- path('payments/status/<uuid:pk>/', PaymentStatusView.as_view(), name='payment-status'),
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 in current directory and parents.
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
- import os
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 = Path.cwd()
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
- if self.config._base_dir:
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
- if self.config._base_dir:
65
- settings.update({
66
- "STATIC_ROOT": self.config._base_dir / "staticfiles",
67
- "MEDIA_ROOT": self.config._base_dir / "media",
68
- "STATICFILES_DIRS": [
69
- self.config._base_dir / "static",
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": "drf_spectacular.openapi.AutoSchema",
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
- """Configure security settings for development."""
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
- self.csrf_trusted_origins = []
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
- cleaned_op_id = operation_id
403
- app_prefixes_to_strip = [
404
- 'django_cfg_newsletter_',
405
- 'django_cfg_accounts_',
406
- 'django_cfg_leads_',
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
- # Try to remove normalized tag as prefix
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
- if api_call_params:
94
- lines.append(f" const response = await {api_call_instance}({api_call_params})")
95
- else:
96
- lines.append(f" const response = await {api_call_instance}()")
97
-
98
- # Validation with Zod
99
- if response_schema:
100
- lines.append(f" return {response_schema}.parse(response)")
101
- else:
102
- lines.append(" return response")
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
- users_list (GET) -> getUsersList
114
- users_retrieve (GET) -> getUsersById
115
- users_create (POST) -> createUsers
116
- users_update (PUT) -> updateUsers
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
- # Remove tag prefix from operation_id
121
- op_id = operation.operation_id
122
-
123
- # Handle common patterns - keep full resource name for uniqueness
124
- if op_id.endswith("_list"):
125
- resource = op_id.removesuffix("_list")
126
- return f"get{self._to_pascal_case(resource)}List"
127
- elif op_id.endswith("_retrieve"):
128
- resource = op_id.removesuffix("_retrieve")
129
- # Add ById suffix to distinguish from list
130
- return f"get{self._to_pascal_case(resource)}ById"
131
- elif op_id.endswith("_create"):
132
- resource = op_id.removesuffix("_create")
133
- return f"create{self._to_pascal_case(resource)}"
134
- elif op_id.endswith("_partial_update"):
135
- resource = op_id.removesuffix("_partial_update")
136
- return f"partialUpdate{self._to_pascal_case(resource)}"
137
- elif op_id.endswith("_update"):
138
- resource = op_id.removesuffix("_update")
139
- return f"update{self._to_pascal_case(resource)}"
140
- elif op_id.endswith("_destroy"):
141
- resource = op_id.removesuffix("_destroy")
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
- # Custom action - use operation_id as is
145
- return self._to_camel_case(op_id)
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
- # Get tag/resource name
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 from operation_id
335
- method_name = self.base.remove_tag_prefix(operation.operation_id, tag)
336
- method_name = self._to_camel_case(method_name)
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,
@@ -327,7 +327,7 @@ class TypeScriptGenerator(BaseGenerator):
327
327
 
328
328
  # Generate individual schema files
329
329
  for schema_name, schema in sorted(all_schemas.items()):
330
- # Skip enum schemas (they use z.nativeEnum from enums.ts)
330
+ # Skip enum schemas (they use z.enum() with literal values)
331
331
  if schema.enum:
332
332
  continue
333
333