Metadata-Version: 2.4
Name: custom_timezone_fdg
Version: 0.1.1
Summary: Configurable business time zones and DST-aware datetime utilities, with optional Django integration
Author: Hua Chen
License-Expression: MIT
Project-URL: Homepage, https://github.com/huachenlihx/custom_timezone_fdg
Project-URL: Repository, https://github.com/huachenlihx/custom_timezone_fdg
Project-URL: Issues, https://github.com/huachenlihx/custom_timezone_fdg/issues
Keywords: timezone,zoneinfo,datetime,dst,django
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Framework :: Django
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: tzdata
Provides-Extra: django
Requires-Dist: Django<7.0,>=4.2; extra == "django"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

# Custom Timezone FDG

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**.

- **Distribution:** `custom_timezone_fdg`
- **Python import:** `custom_timezone`
- **Version:** `0.1.1` (set in `src/custom_timezone/__init__.py` before publishing this release)
- **License:** MIT

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.

## Features

- Configurable business time zone using an explicit override, process configuration, Django setting or environment variable.
- Parsing and formatting datetimes with IANA time-zone names, including `America/Toronto`.
- Explicit handling of **nonexistent** and **ambiguous** local times around DST transitions.
- Thread- and asyncio-task-local temporary time-zone overrides.
- Optional Django middleware that activates the configured time zone per request and restores the previous one afterward.
- Django system checks for invalid time-zone settings and disabled timezone awareness.
- Type information via `py.typed` and compatibility imports through `custom_timezone.timezone_utils`.

## Requirements and installation

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. The GitHub Actions test matrix covers Python 3.9–3.13, including supported Django 4.2, 5.2 and 6.x combinations. Not every possible Python/Django combination is included.

Install the core utilities:

```bash
python -m pip install custom_timezone_fdg
```

Install with Django integration dependencies:

```bash
python -m pip install "custom_timezone_fdg[django]"
```

The **distribution name** contains `_fdg`, but imports use `custom_timezone`:

```python
import custom_timezone as ct
```

If you're contributing from this repository, install it in editable mode:

```bash
python -m pip install -e ".[test,django]"
```

## Quick start (without Django)

```python
import custom_timezone as ct

# Set the business time zone for this Python process.
ct.configure("America/Toronto")

# Interpret a wall-clock time entered without an offset.
appointment = ct.parse_local_datetime("2026-09-26T14:30")
print(appointment)                    # 2026-09-26 14:30:00-04:00
print(ct.format_datetime(appointment))  # 2026-09-26 14:30:00 EDT

# Convert a UTC instant to the configured business time zone.
from datetime import datetime, timezone
stored_utc = datetime(2026, 1, 1, 17, 0, tzinfo=timezone.utc)
print(ct.to_custom_timezone(stored_utc))    # 2026-01-01 12:00:00-05:00
print(ct.format_datetime_local(stored_utc)) # 2026-01-01T12:00

# Use another zone temporarily (also works in async tasks).
with ct.override("Asia/Shanghai"):
    print(ct.now())
```

### Choose the business time zone

The first available value is used:

| Priority | Configuration | Scope |
| --- | --- | --- |
| 1 | `with ct.override("Asia/Shanghai")` | Current thread/asyncio task |
| 2 | `ct.configure("America/Toronto")` | Python process |
| 3 | Django `CUSTOM_TIME_ZONE` | If Django settings are already loaded |
| 4 | `CUSTOM_TIME_ZONE` environment variable | Python process |
| 5 | `UTC` | Default fallback |

Configure a non-Django application using an environment variable:

```bash
export CUSTOM_TIME_ZONE=America/Toronto
```

Or in Python:

```python
ct.configure("America/Toronto")
ct.configure(None)  # Clear the process override; use other configuration sources.
```

An invalid time-zone name raises `TimezoneConfigurationError`, which subclasses `ValueError`.

## Core API

| Function | Purpose |
| --- | --- |
| `get_timezone()` | Return the active `zoneinfo.ZoneInfo` |
| `configure(tz)` | Set or clear the process-wide time zone |
| `override(tz)` | Temporarily use another time zone |
| `parse_local_datetime(text, ...)` | Parse ISO 8601 datetime text into an aware datetime |
| `localize_datetime(dt, ...)` | Attach the selected time zone to a naive datetime |
| `to_custom_timezone(dt, ...)` | Convert a datetime to the configured time zone |
| `format_datetime(dt, ...)` | Format a datetime in the configured time zone |
| `format_datetime_local(dt, ...)` | Format for HTML `datetime-local` input (`YYYY-MM-DDTHH:MM`) |
| `now()` | Return the current time in the configured time zone |

Most helpers also accept `tz="Europe/Paris"` to select a zone for a single call.

### Parsing rules

`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`.

A date without a time is intentionally rejected by default:

```python
ct.parse_local_datetime("2026-09-26")  # ValueError
ct.parse_local_datetime("2026-09-26", allow_date_only=True)  # Local midnight
```

### Daylight-saving transitions

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.

```python
from datetime import datetime

missing = datetime(2026, 3, 8, 2, 30)
ct.localize_datetime(missing, nonexistent="raise")          # Default: NonExistentTimeError
ct.localize_datetime(missing, nonexistent="shift_forward") # 03:30 EDT
ct.localize_datetime(missing, nonexistent="shift_backward")# 01:30 EST

repeated = datetime(2026, 11, 1, 1, 30)
ct.localize_datetime(repeated, ambiguous="earliest")  # First occurrence (EDT)
ct.localize_datetime(repeated, ambiguous="latest")    # Second occurrence (EST)
ct.localize_datetime(repeated, ambiguous="raise")     # AmbiguousTimeError
```

Both exceptions subclass `ValueError`; the same options are available in `parse_local_datetime()`.

### Naive datetimes

With `to_custom_timezone()`, use the `naive=` parameter to specify how an unaware datetime should be interpreted: `"local"` (default), `"utc"`, or `"raise"`.

## Optional Django integration

Install with the `django` extra and configure your new or existing Django project:

```python
# settings.py
import os

INSTALLED_APPS = [
    # Django's standard and project apps...
    "custom_timezone.apps.CustomTimezoneConfig",
]

MIDDLEWARE = [
    # Normal Django middleware...
    "custom_timezone.middleware.CustomTimezoneMiddleware",
]

TIME_ZONE = "UTC"
USE_TZ = True
CUSTOM_TIME_ZONE = os.getenv("CUSTOM_TIME_ZONE", "America/Toronto")
```

**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.

- The middleware activates the selected business time zone for each request, including Django templates, forms and Admin; the previous active zone is restored afterward.
- Django can keep storing timestamps in UTC when `USE_TZ=True`.
- `python manage.py check` reports `custom_timezone.E001` for an invalid configured zone and `custom_timezone.W001` if `USE_TZ=False`.
- This Django app has no models or migrations.

Verify with:

```bash
python manage.py check
python manage.py test
```

### Using in other frameworks

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.

## Replacing an existing in-project `custom_timezone` app

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**.

1. Remove or rename the old in-project `custom_timezone/` module so it cannot shadow the installed distribution.
2. Install `custom_timezone_fdg[django]` in your project's environment.
3. Retain or adapt your existing `INSTALLED_APPS`, middleware, imports and `CUSTOM_TIME_ZONE` setting.
4. Run tests for datetime parsing, API validation and DST transitions before deploying.

| Input/behaviour | Older in-project app | Package behaviour |
| --- | --- | --- |
| Nonexistent local time | Could silently shift forward | Raises `NonExistentTimeError` by default |
| Date-only string | Could be treated as midnight | Rejected unless `allow_date_only=True` |
| Invalid non-string input | Could raise `TypeError` or act as empty | Raises `ValueError` |
| Invalid business time zone | Could raise `ImproperlyConfigured` | Raises `TimezoneConfigurationError` |
| Middleware cleanup | Could reset to Django default | Restores previously active time zone |

## Development, testing and publishing

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`.

```bash
python3.13 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[test,django]"
python -m pytest
```

The package version is defined in `src/custom_timezone/__init__.py` through `__version__`. For the `v0.1.1` release, set `__version__ = "0.1.1"` and commit the change before creating the Git tag. The repository's `CHANGELOG.md` records release details. GitHub Actions runs the supported test matrix on pushes and pull requests; the `main` branch passed the matrix following the CI compatibility fixes.

Build and inspect the distributions:

```bash
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
unzip -l dist/*.whl
```

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.

## Project links

- Source repository: [GitHub](https://github.com/huachenlihx/custom_timezone_fdg)
- Issues and feature requests: [GitHub Issues](https://github.com/huachenlihx/custom_timezone_fdg/issues)

These links assume the repository is published under the shown name; update them if you choose a different repository name.

## Contact

**Email:** `huachenlihx@gmail.com`

## License

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.
