django-cfg 2.3.331__py3-none-any.whl → 2.3.332__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/CHANGELOG.md CHANGED
@@ -108,6 +108,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
108
108
  public names import from it as before.
109
109
 
110
110
  ### Fixed
111
+
112
+ - **API datetimes were shifted by the visitor's UTC offset while labelled
113
+ `Z`.** `REST_FRAMEWORK["DATETIME_FORMAT"]` is `%Y-%m-%dT%H:%M:%SZ` (literal
114
+ `Z`), but DRF converts an aware datetime to the *active* timezone before
115
+ formatting, and `TimezoneMiddleware` activates the visitor's zone from
116
+ `X-Timezone` (the generated TS SDK sends it). A visitor at +08:00 got local
117
+ wall-clock time marked as UTC: `expires_at` 8 hours late, timers firing 8
118
+ hours late. Plain curl without the header was correct, a browser was not.
119
+ - Fix: `DateTimeField.to_representation` now renders under
120
+ `timezone.override(UTC)`. The wire format is unchanged (literal `Z`).
121
+ - Chosen over a serializer/base-class override because patching DRF's own
122
+ `DateTimeField` covers plain `serializers.DateTimeField()`, ModelSerializer
123
+ fields and subclasses with no project changes. It is applied when
124
+ `django_cfg.middleware.timezone` is imported (that is, whenever the
125
+ middleware is installed); `AppConfig.ready` is not used because
126
+ `django_cfg/apps/` shadows `apps.py`, so `ready()` never runs.
127
+ - Not changed: `TimezoneMiddleware` still activates the zone for templates,
128
+ admin and ORM-side code. Input parsing is untouched, so a naive datetime
129
+ in a request body is still read as visitor-local. A field with an explicit
130
+ `default_timezone=` keeps it.
131
+ - Visible effect: clients that worked around the shift will now show
132
+ correct times; clients that did not were showing wrong ones.
111
133
  - **A schedule removed from the config no longer keeps firing.** rq-scheduler
112
134
  keeps a job until something cancels it. Before this fix a dropped
113
135
  `RQScheduleConfig` stayed in Redis, and once its function was deleted it
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__ = "2.3.331"
35
+ __version__ = "2.3.332"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Setup warnings debug early (checks env var only at this point)
@@ -151,5 +151,7 @@ def setup_warnings_debug(
151
151
  message=pattern
152
152
  )
153
153
 
154
- print(f"🔍 Django-CFG Warnings Debug enabled for: {[c.__name__ for c in categories]}")
155
- print(f" Patterns: {patterns}")
154
+ # stderr, not stdout: management commands such as `fleet_enroll_token` print machine-readable
155
+ # output on stdout, and this banner broke pipes and polluted captured values.
156
+ print(f"🔍 Django-CFG Warnings Debug enabled for: {[c.__name__ for c in categories]}", file=sys.stderr)
157
+ print(f" Patterns: {patterns}", file=sys.stderr)
@@ -11,6 +11,10 @@ Persistence: when an authenticated user's timezone differs from what is
11
11
  stored on their profile, the field is updated via a single UPDATE query.
12
12
  A 24-hour in-process cache (keyed by user_pk + tz string) prevents
13
13
  hitting the DB on every request.
14
+
15
+ API output: DRF datetimes always render in UTC (see
16
+ ``django_cfg.startup.drf_datetime_utc``); the active zone still drives
17
+ templates, admin and ORM-side code.
14
18
  """
15
19
 
16
20
  import logging
@@ -20,6 +24,8 @@ from datetime import datetime
20
24
  from django.contrib.auth import get_user_model
21
25
  from django.utils import timezone as dj_timezone
22
26
 
27
+ from django_cfg.startup.drf_datetime_utc import patch_drf_datetime_utc
28
+
23
29
  logger = logging.getLogger(__name__)
24
30
 
25
31
  # In-process cache: {user_pk: tz_string} — refreshed when tz changes or
@@ -27,6 +33,11 @@ logger = logging.getLogger(__name__)
27
33
  _PERSISTED: dict[int, str] = {}
28
34
  _PERSIST_MAX = 2000
29
35
 
36
+ # The middleware activates the visitor zone; DRF would then render API
37
+ # datetimes in it under a literal "Z". Pin API output to UTC, as soon as
38
+ # the middleware exists (AppConfig.ready is not wired for django_cfg).
39
+ patch_drf_datetime_utc()
40
+
30
41
 
31
42
  class TimezoneMiddleware:
32
43
  def __init__(self, get_response):
django_cfg/pyproject.toml CHANGED
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
10
10
 
11
11
  [project]
12
12
  name = "django-cfg"
13
- version = "2.3.331"
13
+ version = "2.3.332"
14
14
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration,\nand 8 enterprise apps. Replace settings.py with validated models, \n90% less code. Production-ready with AI agents, auto-generated TypeScript clients, \nand zero-config features.\n"
15
15
  readme = "README.md"
16
16
  keywords = [
@@ -0,0 +1,46 @@
1
+ """
2
+ django_cfg.startup.drf_datetime_utc — render DRF datetimes in UTC.
3
+
4
+ django-cfg sets ``REST_FRAMEWORK["DATETIME_FORMAT"] = "%Y-%m-%dT%H:%M:%SZ"``
5
+ (literal ``Z``). DRF's ``DateTimeField.to_representation`` converts an aware
6
+ value to the *active* timezone before formatting, and ``TimezoneMiddleware``
7
+ activates the visitor's zone from ``X-Timezone``. Result: local wall-clock
8
+ time labelled ``Z`` (a +08:00 visitor got every datetime 8 hours late).
9
+
10
+ Fix: render under ``timezone.override(UTC)``. Only the output path is wrapped
11
+ (``to_representation``). Input parsing (``enforce_timezone`` from
12
+ ``to_internal_value``) still uses the active zone, so naive input keeps being
13
+ read as visitor-local. Fields with an explicit ``default_timezone=`` argument
14
+ keep it. Django admin, templates and non-API code are not touched.
15
+
16
+ Patching the base class makes this global for plain ``serializers.DateTimeField``
17
+ as well as ModelSerializer fields and subclasses, with no per-project changes.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import datetime
23
+
24
+ _MARKER = "_django_cfg_utc_output"
25
+
26
+
27
+ def patch_drf_datetime_utc() -> None:
28
+ """Idempotently wrap DateTimeField.to_representation. Called from AppConfig.ready."""
29
+ try:
30
+ from django.utils import timezone
31
+ from rest_framework.fields import DateTimeField
32
+ except Exception:
33
+ return
34
+
35
+ original = DateTimeField.to_representation
36
+ if getattr(original, _MARKER, False):
37
+ return
38
+
39
+ def to_representation(self, value):
40
+ with timezone.override(datetime.timezone.utc):
41
+ return original(self, value)
42
+
43
+ setattr(to_representation, _MARKER, True)
44
+ to_representation.__wrapped__ = original
45
+ to_representation.__doc__ = original.__doc__
46
+ DateTimeField.to_representation = to_representation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 2.3.331
3
+ Version: 2.3.332
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, 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=gSeoEbrzo7fuvRqYnqA4RHJ7N0iSZDD4p5AEp8ZDul4,13794
2
+ django_cfg/__init__.py,sha256=vVFu-HCfQWfnUwJwLOtfsNi7bettA2Mh7Pds4SS6AEc,13794
3
3
  django_cfg/apps.py,sha256=7iA6XQWzWgh_IkCt2D5gIdDGETBwPZbMk1NGPBmC6-o,1709
4
4
  django_cfg/config.py,sha256=XMAbOok7fRd6Yu1VvwpfVpxczfTGXAE4S1YGwF-bnTs,2177
5
5
  django_cfg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -371,7 +371,7 @@ django_cfg/core/builders/security_builder.py,sha256=TBWUaE28sKjObKFYgsKz1Bh3BxMN
371
371
  django_cfg/core/debug/README.md,sha256=zdASNAvWr1nOijngul2AY6nAPYxBjanYIXveLHmX5bA,2968
372
372
  django_cfg/core/debug/__init__.py,sha256=Y3660Ouzcg9sCYotqtnpcY1XaBb-u8oa5ga_TfRevi4,127
373
373
  django_cfg/core/debug/example_config.py,sha256=KSi-Ggy2HYAmXG2UssFoJv_2ctWPeDbK6w3lxMPA6oc,1758
374
- django_cfg/core/debug/warnings_helper.py,sha256=-3fTf_pxjayiBCwf21LUPtIyO3CFfDsJCuRaVCoJ0S4,6515
374
+ django_cfg/core/debug/warnings_helper.py,sha256=saok5ryFe1992Xn0FQVK-rJjxpb57xJuiMT41mK0WGw,6729
375
375
  django_cfg/core/decorators/__init__.py,sha256=8vh2p5q8Pary1O0wL4l03ZU3C54bdRTXA8AUOye32t8,464
376
376
  django_cfg/core/decorators/cors.py,sha256=mGb5USTiRPA86tu65NqHX7pWwCOY9a0jeKyTPQgPgO4,5229
377
377
  django_cfg/core/decorators/rate_limit.py,sha256=Kms7331rAnHTgR1SfNvZUZUTK_XdRfa3k-Z_C64di90,15742
@@ -495,7 +495,7 @@ django_cfg/middleware/pool_cleanup.py,sha256=3_FcOJWeQAklF1PTnHftlNIqkXtFGs4XyVt
495
495
  django_cfg/middleware/public_endpoints.py,sha256=gr3yHyQhrfbLQgNBSsynNOi0DvChwdcIVkkpRXg9C2M,6665
496
496
  django_cfg/middleware/real_ip.py,sha256=7XKBH5s7FDkLvt-cxuwbC52RFzzsQmDupXk8CI4H79Y,1297
497
497
  django_cfg/middleware/route_debug.py,sha256=8B-Rg37r_mElwyr5x7gBoFfqgfA0jjztnj9oQH5And4,3675
498
- django_cfg/middleware/timezone.py,sha256=YTd67dquBFFF7q1zDMywCbWQviKMw-d0vh5zdospZO8,3792
498
+ django_cfg/middleware/timezone.py,sha256=VoiL3ORbllNTID55BJ5uSC1KBC4Z_9ZHoZQiLGdu0jo,4265
499
499
  django_cfg/middleware/user_activity.py,sha256=w8ebdAhRyw5rKqN2pK8rOyyVf0xXospp8UYNm0Hd5WI,5742
500
500
  django_cfg/mixins/__init__.py,sha256=1ydS3llsmRcIPljS74vG9Mg01vQ_uBr_-uJdeZZ6Tq4,346
501
501
  django_cfg/mixins/admin_api.py,sha256=zTAhb33Zd_0ya-DqKIXdEN-otY_P6KBvXgkdcrLGHBk,1170
@@ -1318,6 +1318,7 @@ django_cfg/routing/routers.py,sha256=ozQAtnUhQJZlWSBaBMlMx7l2uZ-jQGGfVyYpflRVqro
1318
1318
  django_cfg/startup/__init__.py,sha256=VoHpxOmN8gjRx_QTsdzj17IOK5qrVtxbTyP4fHTRcak,88
1319
1319
  django_cfg/startup/command_shadowing.py,sha256=SgU11QoL1ieAJHRyTW1P99dw2GEyhwsAmn-mVAMFV5E,2853
1320
1320
  django_cfg/startup/constance.py,sha256=Tw-5GQzq_7IkYCNV_eO4KAwoBjeMq6wlUp5v2JdH83s,811
1321
+ django_cfg/startup/drf_datetime_utc.py,sha256=6KoJT05OPwbp7NLb3DjAv3CQNjmdVoU1IGQN6tUvw2g,1788
1321
1322
  django_cfg/startup/drf_patch.py,sha256=Io8lzPDi73wlsbisRmm2gIMQyEGB2YCNrocI9HkwgvE,505
1322
1323
  django_cfg/startup/mime_types.py,sha256=XG0r8-UPSs6LENLVs29ULThFSHJ_jj4ReQaQO_IwT2g,2209
1323
1324
  django_cfg/startup/registry_validation.py,sha256=ZNSQlLF9l8SAUDqzskxU0CHgR_-c9hghStBHW55eLYU,2795
@@ -1361,12 +1362,12 @@ django_cfg/utils/path_resolution.py,sha256=2n0I04lQkSssFaELu3A93YyMAl1K10KPdpxMt
1361
1362
  django_cfg/utils/pool_monitor.py,sha256=AiYdWo-6Q00MT-fRU_wzoc8vzvWpZZi-G00BYf0qZKI,11523
1362
1363
  django_cfg/utils/smart_defaults.py,sha256=st_UXvhMHhxg3WwBWdnagA__zPlppcjececmJzt8m-8,17149
1363
1364
  django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa5jlw,4711
1364
- django_cfg/CHANGELOG.md,sha256=JhtJUqHyE9ZU3U_VyGQ6R9LLed_MuipLV9uVqTMGXpg,11264
1365
+ django_cfg/CHANGELOG.md,sha256=UQFQ18CjCVrX_uF8PkOE5NVs9Vckz6xtcpjMUZoGIEA,12767
1365
1366
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1366
1367
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1367
- django_cfg/pyproject.toml,sha256=Y5xcER_oA0P50-I9vwB_w7TU7HBpyis6Q2ByycByNRU,22134
1368
- django_cfg-2.3.331.dist-info/METADATA,sha256=WLA-0JyZSJYFT0fjZvSaE0VQHhqgFIUpTPM9omllidE,17297
1369
- django_cfg-2.3.331.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
1370
- django_cfg-2.3.331.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1371
- django_cfg-2.3.331.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1372
- django_cfg-2.3.331.dist-info/RECORD,,
1368
+ django_cfg/pyproject.toml,sha256=wHNPanD9E2QDvPRaiJJki2OzmCBmq7BKoW3sOQAodps,22134
1369
+ django_cfg-2.3.332.dist-info/METADATA,sha256=7tuuvH-FsOCm9JIGzCOEt276_L6iBuJQ176JQ54sFHg,17297
1370
+ django_cfg-2.3.332.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
1371
+ django_cfg-2.3.332.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1372
+ django_cfg-2.3.332.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1373
+ django_cfg-2.3.332.dist-info/RECORD,,