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.
- custom_timezone_fdg-0.1.0/CHANGELOG.md +14 -0
- custom_timezone_fdg-0.1.0/LICENSE +21 -0
- custom_timezone_fdg-0.1.0/MANIFEST.in +4 -0
- custom_timezone_fdg-0.1.0/PKG-INFO +280 -0
- custom_timezone_fdg-0.1.0/README.md +248 -0
- custom_timezone_fdg-0.1.0/pyproject.toml +58 -0
- custom_timezone_fdg-0.1.0/setup.cfg +4 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/__init__.py +40 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/apps.py +45 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/config.py +120 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/core.py +224 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/exceptions.py +22 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/middleware.py +43 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/py.typed +0 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone/timezone_utils.py +20 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone_fdg.egg-info/PKG-INFO +280 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone_fdg.egg-info/SOURCES.txt +22 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone_fdg.egg-info/dependency_links.txt +1 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone_fdg.egg-info/requires.txt +7 -0
- custom_timezone_fdg-0.1.0/src/custom_timezone_fdg.egg-info/top_level.txt +1 -0
- custom_timezone_fdg-0.1.0/tests/test_config.py +106 -0
- custom_timezone_fdg-0.1.0/tests/test_core.py +274 -0
- custom_timezone_fdg-0.1.0/tests/test_django_integration.py +156 -0
- custom_timezone_fdg-0.1.0/tests/test_no_django.py +38 -0
|
@@ -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,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,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
|
+
]
|