custom-timezone-fdg 0.1.0__tar.gz

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.
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First public release. Rewrite of the in-project Django app as a framework-independent package.
6
+
7
+ - Core helpers use only `datetime` and `zoneinfo`; Django is optional.
8
+ - Time zone resolution: `override()` > `configure()` > Django setting > `CUSTOM_TIME_ZONE` env var > UTC.
9
+ - Explicit daylight-saving policies (`ambiguous=`, `nonexistent=`) instead of silent guessing.
10
+ - `parse_local_datetime`: version-independent ISO 8601 grammar, `ValueError` for all bad input
11
+ (including non-strings), date-only strings rejected unless `allow_date_only=True`.
12
+ - New: `configure()`, `override()`, `now()`, `tz=` and `naive=` keywords.
13
+ - Django middleware is async-capable and restores the previously active zone; system checks added.
14
+ - The old import path `custom_timezone.timezone_utils` still works.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hua Chen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ include CHANGELOG.md
4
+ recursive-include src/custom_timezone *.py py.typed
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.4
2
+ Name: custom_timezone_fdg
3
+ Version: 0.1.0
4
+ Summary: Configurable business time zones and DST-aware datetime utilities, with optional Django integration
5
+ Author: Hua Chen
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/huachenlihx/custom_timezone_fdg
8
+ Project-URL: Repository, https://github.com/huachenlihx/custom_timezone_fdg
9
+ Project-URL: Issues, https://github.com/huachenlihx/custom_timezone_fdg/issues
10
+ Keywords: timezone,zoneinfo,datetime,dst,django
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Framework :: Django
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: tzdata
27
+ Provides-Extra: django
28
+ Requires-Dist: Django<7.0,>=4.2; extra == "django"
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=7; extra == "test"
31
+ Dynamic: license-file
32
+
33
+ # Custom Timezone FDG
34
+
35
+ A reusable Python package for working with a **configurable business time zone**, including daylight-saving-time (DST) edge cases. It can be used in regular Python programs, background workers and web applications, with **optional Django app and middleware integration**.
36
+
37
+ - **Distribution:** `custom_timezone_fdg`
38
+ - **Python import:** `custom_timezone`
39
+ - **Initial package version:** `0.1.0` (as defined in `src/custom_timezone/__init__.py`)
40
+ - **License:** MIT
41
+
42
+ The core uses Python's `datetime` and `zoneinfo`; **Django is not required** to use the core helpers. The distribution also depends on `tzdata`, which supplies IANA time-zone data on platforms that do not provide it themselves.
43
+
44
+ ## Features
45
+
46
+ - Configurable business time zone using an explicit override, process configuration, Django setting or environment variable.
47
+ - Parsing and formatting datetimes with IANA time-zone names, including `America/Toronto`.
48
+ - Explicit handling of **nonexistent** and **ambiguous** local times around DST transitions.
49
+ - Thread- and asyncio-task-local temporary time-zone overrides.
50
+ - Optional Django middleware that activates the configured time zone per request and restores the previous one afterward.
51
+ - Django system checks for invalid time-zone settings and disabled timezone awareness.
52
+ - Type information via `py.typed` and compatibility imports through `custom_timezone.timezone_utils`.
53
+
54
+ ## Requirements and installation
55
+
56
+ The package declares **Python 3.9+**. Optional Django support declares **Django 4.2 through 6.x**, subject to the Python versions supported by each Django release. Test your particular Python/Django combination; the declared ranges do not mean that every combination has been independently verified.
57
+
58
+ Install the core utilities:
59
+
60
+ ```bash
61
+ python -m pip install custom_timezone_fdg
62
+ ```
63
+
64
+ Install with Django integration dependencies:
65
+
66
+ ```bash
67
+ python -m pip install "custom_timezone_fdg[django]"
68
+ ```
69
+
70
+ The **distribution name** contains `_fdg`, but imports use `custom_timezone`:
71
+
72
+ ```python
73
+ import custom_timezone as ct
74
+ ```
75
+
76
+ If you're contributing from this repository, install it in editable mode:
77
+
78
+ ```bash
79
+ python -m pip install -e ".[test,django]"
80
+ ```
81
+
82
+ ## Quick start (without Django)
83
+
84
+ ```python
85
+ import custom_timezone as ct
86
+
87
+ # Set the business time zone for this Python process.
88
+ ct.configure("America/Toronto")
89
+
90
+ # Interpret a wall-clock time entered without an offset.
91
+ appointment = ct.parse_local_datetime("2026-09-26T14:30")
92
+ print(appointment) # 2026-09-26 14:30:00-04:00
93
+ print(ct.format_datetime(appointment)) # 2026-09-26 14:30:00 EDT
94
+
95
+ # Convert a UTC instant to the configured business time zone.
96
+ from datetime import datetime, timezone
97
+ stored_utc = datetime(2026, 1, 1, 17, 0, tzinfo=timezone.utc)
98
+ print(ct.to_custom_timezone(stored_utc)) # 2026-01-01 12:00:00-05:00
99
+ print(ct.format_datetime_local(stored_utc)) # 2026-01-01T12:00
100
+
101
+ # Use another zone temporarily (also works in async tasks).
102
+ with ct.override("Asia/Shanghai"):
103
+ print(ct.now())
104
+ ```
105
+
106
+ ### Choose the business time zone
107
+
108
+ The first available value is used:
109
+
110
+ | Priority | Configuration | Scope |
111
+ | --- | --- | --- |
112
+ | 1 | `with ct.override("Asia/Shanghai")` | Current thread/asyncio task |
113
+ | 2 | `ct.configure("America/Toronto")` | Python process |
114
+ | 3 | Django `CUSTOM_TIME_ZONE` | If Django settings are already loaded |
115
+ | 4 | `CUSTOM_TIME_ZONE` environment variable | Python process |
116
+ | 5 | `UTC` | Default fallback |
117
+
118
+ Configure a non-Django application using an environment variable:
119
+
120
+ ```bash
121
+ export CUSTOM_TIME_ZONE=America/Toronto
122
+ ```
123
+
124
+ Or in Python:
125
+
126
+ ```python
127
+ ct.configure("America/Toronto")
128
+ ct.configure(None) # Clear the process override; use other configuration sources.
129
+ ```
130
+
131
+ An invalid time-zone name raises `TimezoneConfigurationError`, which subclasses `ValueError`.
132
+
133
+ ## Core API
134
+
135
+ | Function | Purpose |
136
+ | --- | --- |
137
+ | `get_timezone()` | Return the active `zoneinfo.ZoneInfo` |
138
+ | `configure(tz)` | Set or clear the process-wide time zone |
139
+ | `override(tz)` | Temporarily use another time zone |
140
+ | `parse_local_datetime(text, ...)` | Parse ISO 8601 datetime text into an aware datetime |
141
+ | `localize_datetime(dt, ...)` | Attach the selected time zone to a naive datetime |
142
+ | `to_custom_timezone(dt, ...)` | Convert a datetime to the configured time zone |
143
+ | `format_datetime(dt, ...)` | Format a datetime in the configured time zone |
144
+ | `format_datetime_local(dt, ...)` | Format for HTML `datetime-local` input (`YYYY-MM-DDTHH:MM`) |
145
+ | `now()` | Return the current time in the configured time zone |
146
+
147
+ Most helpers also accept `tz="Europe/Paris"` to select a zone for a single call.
148
+
149
+ ### Parsing rules
150
+
151
+ `parse_local_datetime()` accepts ISO-style date and time strings with a `T` or space separator, optionally including an offset. An explicit offset is retained. `None` or blank text returns `None`; invalid input raises `ValueError`.
152
+
153
+ A date without a time is intentionally rejected by default:
154
+
155
+ ```python
156
+ ct.parse_local_datetime("2026-09-26") # ValueError
157
+ ct.parse_local_datetime("2026-09-26", allow_date_only=True) # Local midnight
158
+ ```
159
+
160
+ ### Daylight-saving transitions
161
+
162
+ For `America/Toronto` in 2026, **March 8 at 02:30** does not exist because clocks move forward. **November 1 at 01:30** occurs twice when clocks move back.
163
+
164
+ ```python
165
+ from datetime import datetime
166
+
167
+ missing = datetime(2026, 3, 8, 2, 30)
168
+ ct.localize_datetime(missing, nonexistent="raise") # Default: NonExistentTimeError
169
+ ct.localize_datetime(missing, nonexistent="shift_forward") # 03:30 EDT
170
+ ct.localize_datetime(missing, nonexistent="shift_backward")# 01:30 EST
171
+
172
+ repeated = datetime(2026, 11, 1, 1, 30)
173
+ ct.localize_datetime(repeated, ambiguous="earliest") # First occurrence (EDT)
174
+ ct.localize_datetime(repeated, ambiguous="latest") # Second occurrence (EST)
175
+ ct.localize_datetime(repeated, ambiguous="raise") # AmbiguousTimeError
176
+ ```
177
+
178
+ Both exceptions subclass `ValueError`; the same options are available in `parse_local_datetime()`.
179
+
180
+ ### Naive datetimes
181
+
182
+ With `to_custom_timezone()`, use the `naive=` parameter to specify how an unaware datetime should be interpreted: `"local"` (default), `"utc"`, or `"raise"`.
183
+
184
+ ## Optional Django integration
185
+
186
+ Install with the `django` extra and configure your new or existing Django project:
187
+
188
+ ```python
189
+ # settings.py
190
+ import os
191
+
192
+ INSTALLED_APPS = [
193
+ # Django's standard and project apps...
194
+ "custom_timezone.apps.CustomTimezoneConfig",
195
+ ]
196
+
197
+ MIDDLEWARE = [
198
+ # Normal Django middleware...
199
+ "custom_timezone.middleware.CustomTimezoneMiddleware",
200
+ ]
201
+
202
+ TIME_ZONE = "UTC"
203
+ USE_TZ = True
204
+ CUSTOM_TIME_ZONE = os.getenv("CUSTOM_TIME_ZONE", "America/Toronto")
205
+ ```
206
+
207
+ **Add** these entries to your existing `INSTALLED_APPS` and `MIDDLEWARE` lists; do not replace the other middleware that Django requires. The shorter app entry `"custom_timezone"` is also supported.
208
+
209
+ - The middleware activates the selected business time zone for each request, including Django templates, forms and Admin; the previous active zone is restored afterward.
210
+ - Django can keep storing timestamps in UTC when `USE_TZ=True`.
211
+ - `python manage.py check` reports `custom_timezone.E001` for an invalid configured zone and `custom_timezone.W001` if `USE_TZ=False`.
212
+ - This Django app has no models or migrations.
213
+
214
+ Verify with:
215
+
216
+ ```bash
217
+ python manage.py check
218
+ python manage.py test
219
+ ```
220
+
221
+ ### Using in other frameworks
222
+
223
+ No framework-specific integration is required for regular scripts, FastAPI, Flask or task workers. To select a time zone for a single request or task, use `with ct.override("Area/City")` and ensure the context spans only the intended operation.
224
+
225
+ ## Replacing an existing in-project `custom_timezone` app
226
+
227
+ The import paths `custom_timezone` and `custom_timezone.timezone_utils` are retained for common usage. However, the standalone package intentionally changes certain behaviours, so **test before replacing an existing application-specific implementation**.
228
+
229
+ 1. Remove or rename the old in-project `custom_timezone/` module so it cannot shadow the installed distribution.
230
+ 2. Install `custom_timezone_fdg[django]` in your project's environment.
231
+ 3. Retain or adapt your existing `INSTALLED_APPS`, middleware, imports and `CUSTOM_TIME_ZONE` setting.
232
+ 4. Run tests for datetime parsing, API validation and DST transitions before deploying.
233
+
234
+ | Input/behaviour | Older in-project app | Package behaviour |
235
+ | --- | --- | --- |
236
+ | Nonexistent local time | Could silently shift forward | Raises `NonExistentTimeError` by default |
237
+ | Date-only string | Could be treated as midnight | Rejected unless `allow_date_only=True` |
238
+ | Invalid non-string input | Could raise `TypeError` or act as empty | Raises `ValueError` |
239
+ | Invalid business time zone | Could raise `ImproperlyConfigured` | Raises `TimezoneConfigurationError` |
240
+ | Middleware cleanup | Could reset to Django default | Restores previously active time zone |
241
+
242
+ ## Development, testing and publishing
243
+
244
+ This repository uses the **`src/` layout**: distributable code lives in `src/custom_timezone/`, while tests and packaging files are at the repository root. Consumers still import `custom_timezone`—never `src.custom_timezone`.
245
+
246
+ ```bash
247
+ python3.13 -m venv .venv
248
+ source .venv/bin/activate
249
+ python -m pip install --upgrade pip
250
+ python -m pip install -e ".[test,django]"
251
+ python -m pytest
252
+ ```
253
+
254
+ The `0.1.0` version is defined in `src/custom_timezone/__init__.py` through `__version__`. Update that value before creating a future release. The repository's `CHANGELOG.md` records release details.
255
+
256
+ Build and inspect the distributions:
257
+
258
+ ```bash
259
+ python -m pip install --upgrade build twine
260
+ python -m build
261
+ python -m twine check dist/*
262
+ unzip -l dist/*.whl
263
+ ```
264
+
265
+ Before publishing, install the **built wheel** in a separate clean virtual environment and run its import and (if relevant) Django integration checks. Test on TestPyPI before publishing to PyPI; new uploads cannot overwrite an existing version's distribution filename.
266
+
267
+ ## Project links
268
+
269
+ - Source repository: [GitHub](https://github.com/huachenlihx/custom_timezone_fdg)
270
+ - Issues and feature requests: [GitHub Issues](https://github.com/huachenlihx/custom_timezone_fdg/issues)
271
+
272
+ These links assume the repository is published under the shown name; update them if you choose a different repository name.
273
+
274
+ ## Contact
275
+
276
+ **Email:** `huachenlihx@gmail.com`
277
+
278
+ ## License
279
+
280
+ This project is licensed under the **MIT License**. See the [LICENSE](https://github.com/huachenlihx/custom_timezone_fdg/blob/main/LICENSE) file for the full license text.
@@ -0,0 +1,248 @@
1
+ # Custom Timezone FDG
2
+
3
+ A reusable Python package for working with a **configurable business time zone**, including daylight-saving-time (DST) edge cases. It can be used in regular Python programs, background workers and web applications, with **optional Django app and middleware integration**.
4
+
5
+ - **Distribution:** `custom_timezone_fdg`
6
+ - **Python import:** `custom_timezone`
7
+ - **Initial package version:** `0.1.0` (as defined in `src/custom_timezone/__init__.py`)
8
+ - **License:** MIT
9
+
10
+ The core uses Python's `datetime` and `zoneinfo`; **Django is not required** to use the core helpers. The distribution also depends on `tzdata`, which supplies IANA time-zone data on platforms that do not provide it themselves.
11
+
12
+ ## Features
13
+
14
+ - Configurable business time zone using an explicit override, process configuration, Django setting or environment variable.
15
+ - Parsing and formatting datetimes with IANA time-zone names, including `America/Toronto`.
16
+ - Explicit handling of **nonexistent** and **ambiguous** local times around DST transitions.
17
+ - Thread- and asyncio-task-local temporary time-zone overrides.
18
+ - Optional Django middleware that activates the configured time zone per request and restores the previous one afterward.
19
+ - Django system checks for invalid time-zone settings and disabled timezone awareness.
20
+ - Type information via `py.typed` and compatibility imports through `custom_timezone.timezone_utils`.
21
+
22
+ ## Requirements and installation
23
+
24
+ The package declares **Python 3.9+**. Optional Django support declares **Django 4.2 through 6.x**, subject to the Python versions supported by each Django release. Test your particular Python/Django combination; the declared ranges do not mean that every combination has been independently verified.
25
+
26
+ Install the core utilities:
27
+
28
+ ```bash
29
+ python -m pip install custom_timezone_fdg
30
+ ```
31
+
32
+ Install with Django integration dependencies:
33
+
34
+ ```bash
35
+ python -m pip install "custom_timezone_fdg[django]"
36
+ ```
37
+
38
+ The **distribution name** contains `_fdg`, but imports use `custom_timezone`:
39
+
40
+ ```python
41
+ import custom_timezone as ct
42
+ ```
43
+
44
+ If you're contributing from this repository, install it in editable mode:
45
+
46
+ ```bash
47
+ python -m pip install -e ".[test,django]"
48
+ ```
49
+
50
+ ## Quick start (without Django)
51
+
52
+ ```python
53
+ import custom_timezone as ct
54
+
55
+ # Set the business time zone for this Python process.
56
+ ct.configure("America/Toronto")
57
+
58
+ # Interpret a wall-clock time entered without an offset.
59
+ appointment = ct.parse_local_datetime("2026-09-26T14:30")
60
+ print(appointment) # 2026-09-26 14:30:00-04:00
61
+ print(ct.format_datetime(appointment)) # 2026-09-26 14:30:00 EDT
62
+
63
+ # Convert a UTC instant to the configured business time zone.
64
+ from datetime import datetime, timezone
65
+ stored_utc = datetime(2026, 1, 1, 17, 0, tzinfo=timezone.utc)
66
+ print(ct.to_custom_timezone(stored_utc)) # 2026-01-01 12:00:00-05:00
67
+ print(ct.format_datetime_local(stored_utc)) # 2026-01-01T12:00
68
+
69
+ # Use another zone temporarily (also works in async tasks).
70
+ with ct.override("Asia/Shanghai"):
71
+ print(ct.now())
72
+ ```
73
+
74
+ ### Choose the business time zone
75
+
76
+ The first available value is used:
77
+
78
+ | Priority | Configuration | Scope |
79
+ | --- | --- | --- |
80
+ | 1 | `with ct.override("Asia/Shanghai")` | Current thread/asyncio task |
81
+ | 2 | `ct.configure("America/Toronto")` | Python process |
82
+ | 3 | Django `CUSTOM_TIME_ZONE` | If Django settings are already loaded |
83
+ | 4 | `CUSTOM_TIME_ZONE` environment variable | Python process |
84
+ | 5 | `UTC` | Default fallback |
85
+
86
+ Configure a non-Django application using an environment variable:
87
+
88
+ ```bash
89
+ export CUSTOM_TIME_ZONE=America/Toronto
90
+ ```
91
+
92
+ Or in Python:
93
+
94
+ ```python
95
+ ct.configure("America/Toronto")
96
+ ct.configure(None) # Clear the process override; use other configuration sources.
97
+ ```
98
+
99
+ An invalid time-zone name raises `TimezoneConfigurationError`, which subclasses `ValueError`.
100
+
101
+ ## Core API
102
+
103
+ | Function | Purpose |
104
+ | --- | --- |
105
+ | `get_timezone()` | Return the active `zoneinfo.ZoneInfo` |
106
+ | `configure(tz)` | Set or clear the process-wide time zone |
107
+ | `override(tz)` | Temporarily use another time zone |
108
+ | `parse_local_datetime(text, ...)` | Parse ISO 8601 datetime text into an aware datetime |
109
+ | `localize_datetime(dt, ...)` | Attach the selected time zone to a naive datetime |
110
+ | `to_custom_timezone(dt, ...)` | Convert a datetime to the configured time zone |
111
+ | `format_datetime(dt, ...)` | Format a datetime in the configured time zone |
112
+ | `format_datetime_local(dt, ...)` | Format for HTML `datetime-local` input (`YYYY-MM-DDTHH:MM`) |
113
+ | `now()` | Return the current time in the configured time zone |
114
+
115
+ Most helpers also accept `tz="Europe/Paris"` to select a zone for a single call.
116
+
117
+ ### Parsing rules
118
+
119
+ `parse_local_datetime()` accepts ISO-style date and time strings with a `T` or space separator, optionally including an offset. An explicit offset is retained. `None` or blank text returns `None`; invalid input raises `ValueError`.
120
+
121
+ A date without a time is intentionally rejected by default:
122
+
123
+ ```python
124
+ ct.parse_local_datetime("2026-09-26") # ValueError
125
+ ct.parse_local_datetime("2026-09-26", allow_date_only=True) # Local midnight
126
+ ```
127
+
128
+ ### Daylight-saving transitions
129
+
130
+ For `America/Toronto` in 2026, **March 8 at 02:30** does not exist because clocks move forward. **November 1 at 01:30** occurs twice when clocks move back.
131
+
132
+ ```python
133
+ from datetime import datetime
134
+
135
+ missing = datetime(2026, 3, 8, 2, 30)
136
+ ct.localize_datetime(missing, nonexistent="raise") # Default: NonExistentTimeError
137
+ ct.localize_datetime(missing, nonexistent="shift_forward") # 03:30 EDT
138
+ ct.localize_datetime(missing, nonexistent="shift_backward")# 01:30 EST
139
+
140
+ repeated = datetime(2026, 11, 1, 1, 30)
141
+ ct.localize_datetime(repeated, ambiguous="earliest") # First occurrence (EDT)
142
+ ct.localize_datetime(repeated, ambiguous="latest") # Second occurrence (EST)
143
+ ct.localize_datetime(repeated, ambiguous="raise") # AmbiguousTimeError
144
+ ```
145
+
146
+ Both exceptions subclass `ValueError`; the same options are available in `parse_local_datetime()`.
147
+
148
+ ### Naive datetimes
149
+
150
+ With `to_custom_timezone()`, use the `naive=` parameter to specify how an unaware datetime should be interpreted: `"local"` (default), `"utc"`, or `"raise"`.
151
+
152
+ ## Optional Django integration
153
+
154
+ Install with the `django` extra and configure your new or existing Django project:
155
+
156
+ ```python
157
+ # settings.py
158
+ import os
159
+
160
+ INSTALLED_APPS = [
161
+ # Django's standard and project apps...
162
+ "custom_timezone.apps.CustomTimezoneConfig",
163
+ ]
164
+
165
+ MIDDLEWARE = [
166
+ # Normal Django middleware...
167
+ "custom_timezone.middleware.CustomTimezoneMiddleware",
168
+ ]
169
+
170
+ TIME_ZONE = "UTC"
171
+ USE_TZ = True
172
+ CUSTOM_TIME_ZONE = os.getenv("CUSTOM_TIME_ZONE", "America/Toronto")
173
+ ```
174
+
175
+ **Add** these entries to your existing `INSTALLED_APPS` and `MIDDLEWARE` lists; do not replace the other middleware that Django requires. The shorter app entry `"custom_timezone"` is also supported.
176
+
177
+ - The middleware activates the selected business time zone for each request, including Django templates, forms and Admin; the previous active zone is restored afterward.
178
+ - Django can keep storing timestamps in UTC when `USE_TZ=True`.
179
+ - `python manage.py check` reports `custom_timezone.E001` for an invalid configured zone and `custom_timezone.W001` if `USE_TZ=False`.
180
+ - This Django app has no models or migrations.
181
+
182
+ Verify with:
183
+
184
+ ```bash
185
+ python manage.py check
186
+ python manage.py test
187
+ ```
188
+
189
+ ### Using in other frameworks
190
+
191
+ No framework-specific integration is required for regular scripts, FastAPI, Flask or task workers. To select a time zone for a single request or task, use `with ct.override("Area/City")` and ensure the context spans only the intended operation.
192
+
193
+ ## Replacing an existing in-project `custom_timezone` app
194
+
195
+ The import paths `custom_timezone` and `custom_timezone.timezone_utils` are retained for common usage. However, the standalone package intentionally changes certain behaviours, so **test before replacing an existing application-specific implementation**.
196
+
197
+ 1. Remove or rename the old in-project `custom_timezone/` module so it cannot shadow the installed distribution.
198
+ 2. Install `custom_timezone_fdg[django]` in your project's environment.
199
+ 3. Retain or adapt your existing `INSTALLED_APPS`, middleware, imports and `CUSTOM_TIME_ZONE` setting.
200
+ 4. Run tests for datetime parsing, API validation and DST transitions before deploying.
201
+
202
+ | Input/behaviour | Older in-project app | Package behaviour |
203
+ | --- | --- | --- |
204
+ | Nonexistent local time | Could silently shift forward | Raises `NonExistentTimeError` by default |
205
+ | Date-only string | Could be treated as midnight | Rejected unless `allow_date_only=True` |
206
+ | Invalid non-string input | Could raise `TypeError` or act as empty | Raises `ValueError` |
207
+ | Invalid business time zone | Could raise `ImproperlyConfigured` | Raises `TimezoneConfigurationError` |
208
+ | Middleware cleanup | Could reset to Django default | Restores previously active time zone |
209
+
210
+ ## Development, testing and publishing
211
+
212
+ This repository uses the **`src/` layout**: distributable code lives in `src/custom_timezone/`, while tests and packaging files are at the repository root. Consumers still import `custom_timezone`—never `src.custom_timezone`.
213
+
214
+ ```bash
215
+ python3.13 -m venv .venv
216
+ source .venv/bin/activate
217
+ python -m pip install --upgrade pip
218
+ python -m pip install -e ".[test,django]"
219
+ python -m pytest
220
+ ```
221
+
222
+ The `0.1.0` version is defined in `src/custom_timezone/__init__.py` through `__version__`. Update that value before creating a future release. The repository's `CHANGELOG.md` records release details.
223
+
224
+ Build and inspect the distributions:
225
+
226
+ ```bash
227
+ python -m pip install --upgrade build twine
228
+ python -m build
229
+ python -m twine check dist/*
230
+ unzip -l dist/*.whl
231
+ ```
232
+
233
+ Before publishing, install the **built wheel** in a separate clean virtual environment and run its import and (if relevant) Django integration checks. Test on TestPyPI before publishing to PyPI; new uploads cannot overwrite an existing version's distribution filename.
234
+
235
+ ## Project links
236
+
237
+ - Source repository: [GitHub](https://github.com/huachenlihx/custom_timezone_fdg)
238
+ - Issues and feature requests: [GitHub Issues](https://github.com/huachenlihx/custom_timezone_fdg/issues)
239
+
240
+ These links assume the repository is published under the shown name; update them if you choose a different repository name.
241
+
242
+ ## Contact
243
+
244
+ **Email:** `huachenlihx@gmail.com`
245
+
246
+ ## License
247
+
248
+ This project is licensed under the **MIT License**. See the [LICENSE](https://github.com/huachenlihx/custom_timezone_fdg/blob/main/LICENSE) file for the full license text.
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "custom_timezone_fdg"
7
+ dynamic = ["version"]
8
+ description = "Configurable business time zones and DST-aware datetime utilities, with optional Django integration"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Hua Chen" }
14
+ ]
15
+ keywords = ["timezone", "zoneinfo", "datetime", "dst", "django"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Framework :: Django",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Typing :: Typed",
29
+ ]
30
+ # The core uses Python's standard library. tzdata supplies IANA data on
31
+ # platforms without a system time-zone database (e.g., Windows/slim images).
32
+ dependencies = ["tzdata"]
33
+
34
+ [project.optional-dependencies]
35
+ django = ["Django>=4.2,<7.0"]
36
+ test = ["pytest>=7"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/huachenlihx/custom_timezone_fdg"
40
+ Repository = "https://github.com/huachenlihx/custom_timezone_fdg"
41
+ Issues = "https://github.com/huachenlihx/custom_timezone_fdg/issues"
42
+
43
+ [tool.setuptools.dynamic]
44
+ version = { attr = "custom_timezone.__version__" }
45
+
46
+ [tool.setuptools]
47
+ package-dir = {"" = "src"}
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+ include = ["custom_timezone", "custom_timezone.*"]
52
+
53
+ [tool.setuptools.package-data]
54
+ custom_timezone = ["py.typed"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ addopts = "-ra"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ """One configurable business time zone for any Python project.
2
+
3
+ Plain Python, no framework required::
4
+
5
+ import custom_timezone as ct
6
+
7
+ ct.configure("America/Toronto") # or set CUSTOM_TIME_ZONE in the environment
8
+ start = ct.parse_local_datetime("2026-09-26T14:30") # aware, America/Toronto
9
+ ct.format_datetime(start) # '2026-09-26 14:30:00 EDT'
10
+
11
+ Django users additionally get an app config and a middleware (see the README); those live in
12
+ ``custom_timezone.apps`` and ``custom_timezone.middleware`` and are only imported by Django.
13
+ """
14
+ from .config import DEFAULT_TIMEZONE, ENV_VAR, SETTING_NAME, configure, get_timezone, override
15
+ from .core import (
16
+ format_datetime,
17
+ format_datetime_local,
18
+ localize_datetime,
19
+ now,
20
+ parse_local_datetime,
21
+ to_custom_timezone,
22
+ )
23
+ from .exceptions import (
24
+ AmbiguousTimeError,
25
+ CustomTimezoneError,
26
+ NonExistentTimeError,
27
+ TimezoneConfigurationError,
28
+ )
29
+
30
+ __version__ = "0.1.0"
31
+
32
+ __all__ = [
33
+ "__version__",
34
+ "configure", "get_timezone", "override",
35
+ "localize_datetime", "parse_local_datetime", "to_custom_timezone",
36
+ "format_datetime", "format_datetime_local", "now",
37
+ "CustomTimezoneError", "TimezoneConfigurationError",
38
+ "NonExistentTimeError", "AmbiguousTimeError",
39
+ "DEFAULT_TIMEZONE", "ENV_VAR", "SETTING_NAME",
40
+ ]