django-cfg 1.4.96__py3-none-any.whl → 1.4.97__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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.96"
35
+ __version__ = "1.4.97"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -4,8 +4,9 @@ JWT tokens are automatically injected into HTML responses for authenticated user
4
4
  This is specific to Next.js frontend apps only.
5
5
 
6
6
  Features:
7
- - Automatic extraction of ZIP archives with timestamp comparison
8
- - Auto-reextraction when ZIP is newer than extracted directory
7
+ - Automatic extraction of ZIP archives with metadata comparison (size + mtime)
8
+ - Auto-reextraction when ZIP content changes (size or timestamp)
9
+ - Marker file (.zip_meta) tracks ZIP metadata for reliable comparison
9
10
  - Cache busting (no-store headers for HTML)
10
11
  - SPA routing with fallback strategies
11
12
  - JWT token injection for authenticated users
@@ -29,12 +30,13 @@ logger = logging.getLogger(__name__)
29
30
 
30
31
  class ZipExtractionMixin:
31
32
  """
32
- Mixin for automatic ZIP extraction with timestamp-based refresh.
33
+ Mixin for automatic ZIP extraction with metadata-based refresh.
33
34
 
34
35
  Provides intelligent ZIP archive handling:
35
36
  - Auto-extraction when directory doesn't exist
36
- - Auto-reextraction when ZIP is newer than extracted directory
37
- - Timestamp comparison ensures fresh builds are always deployed
37
+ - Auto-reextraction when ZIP metadata changes (size or mtime)
38
+ - Marker file (.zip_meta) tracks ZIP state for reliable comparison
39
+ - Works correctly in Docker where timestamps can be misleading
38
40
 
39
41
  Usage:
40
42
  class MyView(ZipExtractionMixin, View):
@@ -43,12 +45,16 @@ class ZipExtractionMixin:
43
45
 
44
46
  def extract_zip_if_needed(self, base_dir: Path, zip_path: Path, app_name: str) -> bool:
45
47
  """
46
- Extract ZIP archive if needed based on ZIP modification time comparison.
48
+ Extract ZIP archive if needed based on ZIP metadata (size + mtime) comparison.
47
49
 
48
50
  Logic:
49
51
  1. If directory doesn't exist → extract
50
- 2. If ZIP is newer than directory remove and re-extract
51
- 3. If directory is newer than ZIPuse existing
52
+ 2. If marker file doesn't exist → extract
53
+ 3. If ZIP metadata changed (size or mtime) remove and re-extract
54
+ 4. If metadata matches → use existing
55
+
56
+ Uses marker file (.zip_meta) to track ZIP metadata. More reliable than
57
+ just mtime comparison, especially in Docker where timestamps can be misleading.
52
58
 
53
59
  Args:
54
60
  base_dir: Target directory for extraction
@@ -65,30 +71,40 @@ class ZipExtractionMixin:
65
71
  logger.error(f"[{app_name}] ZIP not found: {zip_path}")
66
72
  return False
67
73
 
68
- # Get ZIP modification time
69
- zip_mtime = datetime.fromtimestamp(zip_path.stat().st_mtime)
74
+ # Get ZIP metadata (size + mtime for reliable comparison)
75
+ zip_stat = zip_path.stat()
76
+ current_meta = f"{zip_stat.st_size}:{zip_stat.st_mtime}"
77
+
78
+ # Marker file stores ZIP metadata
79
+ marker_file = base_dir / '.zip_meta'
70
80
 
71
81
  # Priority 1: If directory doesn't exist at all - always extract
72
82
  if not base_dir.exists():
73
83
  should_extract = True
74
84
  logger.info(f"[{app_name}] Directory doesn't exist, will extract")
75
85
 
76
- # Priority 2: Directory exists - compare timestamps
86
+ # Priority 2: Marker file doesn't exist - extract (first run or corrupted)
87
+ elif not marker_file.exists():
88
+ should_extract = True
89
+ logger.info(f"[{app_name}] No marker file found, will extract")
90
+
91
+ # Priority 3: Compare stored metadata with current ZIP metadata
77
92
  else:
78
- # Get directory modification time
79
- dir_mtime = datetime.fromtimestamp(base_dir.stat().st_mtime)
80
-
81
- # If ZIP is newer than directory - re-extract
82
- if zip_mtime > dir_mtime:
83
- logger.info(f"[{app_name}] ZIP is newer (ZIP: {zip_mtime}, DIR: {dir_mtime}), re-extracting")
84
- try:
85
- shutil.rmtree(base_dir)
86
- should_extract = True
87
- except Exception as e:
88
- logger.error(f"[{app_name}] Failed to remove old directory: {e}")
89
- return False
90
- else:
91
- logger.debug(f"[{app_name}] Directory is up-to-date (DIR: {dir_mtime} >= ZIP: {zip_mtime})")
93
+ try:
94
+ stored_meta = marker_file.read_text().strip()
95
+ if stored_meta != current_meta:
96
+ logger.info(f"[{app_name}] ZIP metadata changed (stored: {stored_meta}, current: {current_meta}), re-extracting")
97
+ try:
98
+ shutil.rmtree(base_dir)
99
+ should_extract = True
100
+ except Exception as e:
101
+ logger.error(f"[{app_name}] Failed to remove old directory: {e}")
102
+ return False
103
+ else:
104
+ logger.info(f"[{app_name}] ZIP unchanged (meta: {current_meta}), using existing directory")
105
+ except Exception as e:
106
+ logger.warning(f"[{app_name}] Failed to read marker file: {e}, will re-extract")
107
+ should_extract = True
92
108
 
93
109
  # Extract ZIP if needed
94
110
  if should_extract:
@@ -97,7 +113,10 @@ class ZipExtractionMixin:
97
113
  base_dir.parent.mkdir(parents=True, exist_ok=True)
98
114
  with zipfile.ZipFile(zip_path, 'r') as zip_ref:
99
115
  zip_ref.extractall(base_dir)
100
- logger.info(f"[{app_name}] Successfully extracted {zip_path.name}")
116
+
117
+ # Write marker file with current metadata
118
+ marker_file.write_text(current_meta)
119
+ logger.info(f"[{app_name}] Successfully extracted {zip_path.name} and saved marker (meta: {current_meta})")
101
120
  return True
102
121
  except Exception as e:
103
122
  logger.error(f"[{app_name}] Failed to extract: {e}")
@@ -114,7 +133,7 @@ class NextJSStaticView(ZipExtractionMixin, View):
114
133
 
115
134
  Features:
116
135
  - Serves Next.js static export files like a static file server
117
- - Smart ZIP extraction: auto-refreshes when ZIP is newer than directory
136
+ - Smart ZIP extraction: compares ZIP metadata (size + mtime) with marker file
118
137
  - Automatically injects JWT tokens for authenticated users
119
138
  - Tokens injected into HTML responses only
120
139
  - Handles Next.js client-side routing (.html fallback)
@@ -123,9 +142,10 @@ class NextJSStaticView(ZipExtractionMixin, View):
123
142
 
124
143
  ZIP Extraction Logic:
125
144
  - If directory doesn't exist: extract from ZIP
126
- - If ZIP is newer than directory: remove and re-extract
127
- - If directory is up-to-date: use existing files
128
- - This ensures fresh builds are always deployed automatically
145
+ - If marker file missing: extract from ZIP
146
+ - If ZIP metadata changed: remove and re-extract
147
+ - If metadata matches: use existing files
148
+ - Marker file (.zip_meta) ensures reliable comparison in Docker
129
149
 
130
150
  Path resolution examples:
131
151
  - /cfg/admin/ → /cfg/admin/index.html
@@ -5,8 +5,8 @@ Serves Next.js static files with SPA routing support and JWT injection.
5
5
 
6
6
  Features:
7
7
  - Priority-based ZIP resolution (solution project → package fallback)
8
- - Automatic extraction with timestamp comparison (ZIP mtime vs directory mtime)
9
- - Cache busting (no-store headers + timestamp query params)
8
+ - Automatic extraction with metadata comparison (ZIP size + mtime vs marker file)
9
+ - Cache busting (no-store headers for HTML)
10
10
  - SPA routing with fallback strategies
11
11
  - JWT token injection for authenticated users
12
12
 
@@ -15,8 +15,9 @@ ZIP Resolution Priority:
15
15
  2. Package fallback: django_cfg/static/frontend/nextjs_admin.zip → django_cfg/static/frontend/nextjs_admin/
16
16
 
17
17
  Extraction Logic:
18
- - Compares ZIP mtime with directory mtime
19
- - Re-extracts only when ZIP is newer
18
+ - Marker file (.zip_meta) tracks ZIP metadata (size:mtime)
19
+ - Re-extracts when metadata changes (size or timestamp)
20
+ - Reliable in Docker where timestamps can be misleading
20
21
  - Ensures fresh builds are deployed automatically
21
22
  """
22
23
 
@@ -42,7 +43,7 @@ class NextJsAdminView(ZipExtractionMixin, LoginRequiredMixin, View):
42
43
  Features:
43
44
  - Serves Next.js static build files
44
45
  - Priority-based ZIP resolution (solution first, package fallback)
45
- - Smart ZIP extraction: compares ZIP mtime vs directory mtime
46
+ - Smart ZIP extraction: metadata comparison (size + mtime) with marker file
46
47
  - Cache busting: no-store headers for HTML files
47
48
  - Automatic JWT token injection for authenticated users
48
49
  - SPA routing support (path/to/route → path/to/route/index.html)
@@ -53,9 +54,10 @@ class NextJsAdminView(ZipExtractionMixin, LoginRequiredMixin, View):
53
54
 
54
55
  ZIP Extraction Logic:
55
56
  - If directory doesn't exist: extract from ZIP
56
- - If ZIP is newer than directory: remove and re-extract
57
- - If directory is up-to-date: use existing files
58
- - Ensures fresh builds are deployed automatically
57
+ - If marker file missing: extract from ZIP
58
+ - If ZIP metadata changed: remove and re-extract
59
+ - If metadata matches: use existing files
60
+ - Marker file (.zip_meta) ensures reliable comparison in Docker
59
61
 
60
62
  URL Examples:
61
63
  /cfg/nextjs-admin/admin/ → admin/index.html
django_cfg/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.96"
7
+ version = "1.4.97"
8
8
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
@@ -6,7 +6,6 @@ Provides template tags for accessing django-cfg configuration constants.
6
6
 
7
7
  import os
8
8
  import socket
9
- import time
10
9
  from django import template
11
10
  from django.conf import settings
12
11
  from django.utils.safestring import mark_safe
@@ -184,13 +183,9 @@ def nextjs_admin_url(path=''):
184
183
  # Normalize path - remove leading/trailing slashes
185
184
  path = path.strip('/')
186
185
 
187
- # Add cache busting parameter (timestamp in milliseconds)
188
- cache_buster = f'_={int(time.time() * 1000)}'
189
-
190
186
  if not settings.DEBUG:
191
- # Production mode: always use static files with cache buster
192
- base_url = f'/cfg/admin/admin/{path}' if path else '/cfg/admin/admin/'
193
- return f'{base_url}?{cache_buster}'
187
+ # Production mode: always use static files
188
+ return f'/cfg/admin/{path}' if path else '/cfg/admin/'
194
189
 
195
190
  # Check if port 3001 is available for Tab 1 (built-in admin)
196
191
  port_3001_available = _is_port_available('localhost', 3001)
@@ -198,13 +193,11 @@ def nextjs_admin_url(path=''):
198
193
  if port_3001_available:
199
194
  # Dev server is running on 3001 - use it (builtin admin has no /admin prefix)
200
195
  base_url = 'http://localhost:3001'
201
- url = f'{base_url}/{path}' if path else base_url
202
- return f'{url}?{cache_buster}'
196
+ return f'{base_url}/{path}' if path else base_url
203
197
  else:
204
- # No dev server or dev server stopped - use static files with cache buster
198
+ # No dev server or dev server stopped - use static files
205
199
  # Static files are served from /cfg/admin/ but ZIP contains files in root (no /admin prefix)
206
- base_url = f'/cfg/admin/{path}' if path else '/cfg/admin/'
207
- return f'{base_url}?{cache_buster}'
200
+ return f'/cfg/admin/{path}' if path else '/cfg/admin/'
208
201
 
209
202
 
210
203
  @register.simple_tag
@@ -280,20 +273,15 @@ def nextjs_external_admin_url(route=''):
280
273
 
281
274
  route = route.strip('/')
282
275
 
283
- # Add cache busting parameter (timestamp in milliseconds)
284
- cache_buster = f'_={int(time.time() * 1000)}'
285
-
286
276
  # Auto-detect development mode: DEBUG=True + port 3000 available
287
277
  if settings.DEBUG and _is_port_available('localhost', 3000):
288
278
  # Development mode: solution project on port 3000
289
279
  # Routes start with /admin in Next.js (e.g., /admin, /admin/crypto)
290
280
  base_url = 'http://localhost:3000/admin'
291
- url = f'{base_url}/{route}' if route else base_url
292
- return f'{url}?{cache_buster}'
281
+ return f'{base_url}/{route}' if route else base_url
293
282
  else:
294
283
  # Production mode: use relative URL - Django serves from extracted ZIP with /admin prefix
295
- base_url = f"/cfg/nextjs-admin/admin/{route}" if route else "/cfg/nextjs-admin/admin/"
296
- return f'{base_url}?{cache_buster}'
284
+ return f"/cfg/nextjs-admin/admin/{route}" if route else "/cfg/nextjs-admin/admin/"
297
285
  except Exception:
298
286
  return ''
299
287
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.96
3
+ Version: 1.4.97
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -1,5 +1,5 @@
1
1
  django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_cfg/__init__.py,sha256=UbaqH6yGdQLEgNwn0Hmp7oKhbtKyZTVYU4UIvyIPn_c,1620
2
+ django_cfg/__init__.py,sha256=_L0rVeRDg_vkLAwSqGT8ka-Mc6eZ0ddniv5wfm9RJvM,1620
3
3
  django_cfg/apps.py,sha256=72m3uuvyqGiLx6gOfE-BD3P61jddCCERuBOYpxTX518,1605
4
4
  django_cfg/config.py,sha256=y4Z3rnYsHBE0TehpwAIPaxr---mkvyKrZGGsNwYso74,1398
5
5
  django_cfg/apps/__init__.py,sha256=JtDmEYt1OcleWM2ZaeX0LKDnRQzPOavfaXBWG4ECB5Q,26
@@ -228,7 +228,7 @@ django_cfg/apps/frontend/apps.py,sha256=9eEK0Tq2nOsol7xSK5aobdwTDJTJrWx6Iy1I1DQb
228
228
  django_cfg/apps/frontend/setup.py,sha256=TxKQwQw4xTF6VSyhrQBzbUsdsVQR9JHdjc36LZKeQh4,2444
229
229
  django_cfg/apps/frontend/test_routing.py,sha256=fshJOR9ln7m3gXY9EI1_ix_6E5xua6DR264b16RIF-w,4832
230
230
  django_cfg/apps/frontend/urls.py,sha256=Vz22_2i2w1J0KQYDCxHnTF5rUf32kUUSBDJZrP07XgY,284
231
- django_cfg/apps/frontend/views.py,sha256=SOYOgZdJWnNQLOMLY1Lx7xkGnI_pS4OKV5UNydQsh48,13922
231
+ django_cfg/apps/frontend/views.py,sha256=JPTJRPCGY3zLLiiz1F9CsZiLkXbQ5RfnViJZzBUpV7o,15023
232
232
  django_cfg/apps/frontend/templates/frontend/404.html,sha256=LCFig_dcgDDmYKhgOLu8R2KDs_aQS6Es6rAxLTAEXWs,2175
233
233
  django_cfg/apps/knowbase/README.md,sha256=HXt_J6WCN-LsMhA7p9mdvih07_vp_r_hkPdmqHhNEeo,3965
234
234
  django_cfg/apps/knowbase/__init__.py,sha256=cfGnxDQwjajPhUoleKkgvdabJcB0LdXEglnsBojKkPo,1045
@@ -1021,7 +1021,7 @@ django_cfg/modules/django_unfold/models/navigation.py,sha256=PPEeqA2HBaA1-VjADiX
1021
1021
  django_cfg/modules/nextjs_admin/__init__.py,sha256=lfrZYyNRExH3Z5De8G4hQBIZoFlW5Ejze3couNrztbY,312
1022
1022
  django_cfg/modules/nextjs_admin/apps.py,sha256=HxVUMmWTKdYpwJ00iIfWVFsBzsawsOVhEPZqjk_izjI,347
1023
1023
  django_cfg/modules/nextjs_admin/urls.py,sha256=7n0yStm0WNchw14Rtu_mgsIA3WKQsYP9WZt3-YOUWjU,603
1024
- django_cfg/modules/nextjs_admin/views.py,sha256=4gzdo2gxY2zSs9MxxZc2Z7FfJHGsg5ZcGXWOo4i3sLA,11246
1024
+ django_cfg/modules/nextjs_admin/views.py,sha256=zvRMZZRRcwIA3ie0AEnllOUUwLklbt3nq-P5Eq9BzeY,11385
1025
1025
  django_cfg/modules/nextjs_admin/models/__init__.py,sha256=WGw9KXcYd1O9AoA_bpMoz2gLZUlRzjGmUBjjbObcUi0,100
1026
1026
  django_cfg/modules/nextjs_admin/models/config.py,sha256=0ADqLuiywSCQfx_z9dkwjFCca3lr3F2uQffIjTr_QXw,5864
1027
1027
  django_cfg/modules/nextjs_admin/templatetags/__init__.py,sha256=ChVBnJggCIY8rMhfyJFoA8k0qKo-8FtJknrk54Vx4wM,51
@@ -1076,7 +1076,7 @@ django_cfg/templates/admin/index.html,sha256=RidRvZwc6LFzRi8l6vHBgyM_CD0yvhPWvr4
1076
1076
  django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8G4L_GexPxvz6XQ,8836
1077
1077
  django_cfg/templates/unfold/layouts/skeleton.html,sha256=2ArkcNZ34mFs30cOAsTQ1EZiDXcB0aVxkO71lJq9SLE,718
1078
1078
  django_cfg/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1079
- django_cfg/templatetags/django_cfg.py,sha256=BLkdH6Pl32itRafOpPxXGg1QhZ9huacAAqFv-W8LTkM,10044
1079
+ django_cfg/templatetags/django_cfg.py,sha256=Q1-dQV8SPOuQu1dFg5eT1gT88eEUXloYMz-7_-OHT0c,9522
1080
1080
  django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
1081
1081
  django_cfg/utils/path_resolution.py,sha256=2n0I04lQkSssFaELu3A93YyMAl1K10KPdpxMt5k4Iy0,13341
1082
1082
  django_cfg/utils/smart_defaults.py,sha256=ZUj6K_Deq-fp5O0Dy_Emt257UWFn0f9bkgFv9YCR58U,9239
@@ -1084,9 +1084,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1084
1084
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1085
1085
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1086
1086
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1087
- django_cfg/pyproject.toml,sha256=Npc1mjOUxl_Dvp1uFVjLCbrQo1tlTMMxj4Oemc6kLvo,8572
1088
- django_cfg-1.4.96.dist-info/METADATA,sha256=6LS9GzmiwHVvee547cEZjnwrdkib9Kw9Zja8LowXnk0,23733
1089
- django_cfg-1.4.96.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1090
- django_cfg-1.4.96.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1091
- django_cfg-1.4.96.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1092
- django_cfg-1.4.96.dist-info/RECORD,,
1087
+ django_cfg/pyproject.toml,sha256=0AbWzGlCW_MemwzdLfIeySO-sNszpPYNvkPOTe_DUF4,8572
1088
+ django_cfg-1.4.97.dist-info/METADATA,sha256=cb_cXupXvXVnnbikccyCZZwHhzgsBSOa8OWbx76N524,23733
1089
+ django_cfg-1.4.97.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1090
+ django_cfg-1.4.97.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1091
+ django_cfg-1.4.97.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1092
+ django_cfg-1.4.97.dist-info/RECORD,,