├── timezones ├── py.typed ├── __init__.py ├── zones.py ├── test_timezones.py ├── tz_rendering.py ├── tz_utils.py └── _defs.py ├── setup.cfg ├── tox.ini ├── .gitignore ├── Makefile ├── README.md ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── LICENSE ├── CHANGELOG.md ├── pyproject.toml └── poetry.lock /timezones/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timezones/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = mypy, py311, py312, py313 3 | isolated_build = True 4 | 5 | [gh-actions] 6 | python = 7 | 3.11: py311, mypy 8 | 3.12: py312 9 | 3.13: py313 10 | 11 | [testenv] 12 | deps = 13 | pytest 14 | commands = 15 | pytest {posargs} 16 | 17 | [testenv:mypy] 18 | deps = 19 | mypy 20 | commands = 21 | mypy timezones {posargs:--ignore-missing-imports} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | # Translations 24 | *.mo 25 | 26 | # Mr Developer 27 | .mr.developer.cfg 28 | 29 | # Utils 30 | env 31 | .vscode 32 | 33 | # GeoIP DBs 34 | *.mmdb 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell poetry version --short) 2 | 3 | help: 4 | @echo "Usage: 'make clean' or 'make build' or 'make tag' or 'make upload' or 'make test'" 5 | 6 | 7 | clean: 8 | rm -rf dist/* 9 | 10 | 11 | build: clean 12 | poetry build 13 | 14 | 15 | tag: 16 | git tag -l | grep -q v$(VERSION) || { \ 17 | git tag v$(VERSION) && \ 18 | git push && \ 19 | git push --tags; \ 20 | } 21 | 22 | 23 | upload: tag build 24 | poetry publish 25 | 26 | 27 | test: 28 | poetry run pytest 29 | 30 | 31 | .PHONY: help clean build tag upload test 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-timezones 2 | ---------------- 3 | 4 | ![](https://github.com/Doist/python-timezones/workflows/Tests/badge.svg) 5 | 6 | > [!IMPORTANT] 7 | > This package is in low-priority maintenance mode. The Doist team will address 8 | > bugs and security issues on a best-effort basis, but no new feature will be 9 | > added. 10 | 11 | A Python library that provides better selection of common timezones, with 12 | optional HTML output. 13 | 14 | Visit https://doist.github.io/python-timezones/ for more information. 15 | 16 | Copyright: 2012-2024 by Doist 17 | 18 | License: MIT. 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: mixed-line-ending 8 | - id: check-merge-conflict 9 | - id: check-case-conflict 10 | - id: debug-statements 11 | 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: v0.7.3 14 | hooks: 15 | # Run the linter 16 | - id: ruff 17 | args: ["--fix"] 18 | # Run the formatter 19 | - id: ruff-format 20 | 21 | - repo: https://github.com/pre-commit/mirrors-mypy 22 | rev: v1.13.0 23 | hooks: 24 | - id: mypy 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build-test: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 60 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.11" 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install poetry 23 | 24 | - name: Build and publish to PyPI 25 | env: 26 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }} 27 | run: | 28 | poetry publish --build 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | python-version: 13 | - 3.11 14 | - 3.12 15 | - 3.13 16 | timeout-minutes: 5 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install poetry 29 | poetry install 30 | 31 | - name: Test with tox 32 | run: poetry run tox 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2022 Doist 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. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [3.0.0] - 2024-11-15 10 | ### Changed 11 | - Add support for Python 3.11, 3.12 and 3.13. 12 | - Add notice about the package being in maintenance mode only. 13 | 14 | ### Removed 15 | - Drop support for GeoIP and `guess_timezone_by_ip()`. 16 | - Drop support for Python 3.9 and 3.10. 17 | 18 | ## [2.2.0] - 2023-03-06 19 | ### Added 20 | - Add type hints to all public functions. 21 | 22 | ### Changed 23 | - Use `zoneinfo` (in the standard library since Python 3.9) instead of `pytz`. 24 | - Improved UTC offsets, to ensure they're always up-to-date (according to `zoneinfo`) and never include DST. 25 | 26 | ## Removed 27 | - Drop support for Python 3.8. 28 | 29 | ## [2.1.0] - 2022-07-20 30 | ### Added 31 | - Introduce this changelog 🎉 32 | 33 | ### Changed 34 | - Rename Europe/Kiev to Europe/Kyiv. 35 | - Switch to Poetry for packaging. 36 | - Dependencies: `future` is no longer required, and `geoip2` is now optional. 37 | - Improve CI and dev tooling. 38 | 39 | ### Removed 40 | - Python 2 is no longer supported. 41 | -------------------------------------------------------------------------------- /timezones/zones.py: -------------------------------------------------------------------------------- 1 | """ 2 | zones 3 | ~~~~~~~~ 4 | 5 | Holds a collection of common timezones. 6 | Is much smaller and better formatted than pytz.common_timezones. 7 | It also supports fixed timezones such as `GMT +7:00`. 8 | 9 | Example usage (returns US based timezones):: 10 | 11 | for tz_offset, tz_name, tz_formatted in zones.get_timezones(only_us=True): 12 | print(tz_formatted) 13 | 14 | => 15 | 16 | "(GMT-1000) Hawaii')" 17 | "(GMT-0900) Alaska')" 18 | "(GMT-0800) Pacific Time (US & Canada)" 19 | ... 20 | 21 | :copyright: 2012 by Amir Salihefendic ( http://amix.dk/ ) 22 | :license: MIT 23 | """ 24 | from __future__ import annotations 25 | 26 | from . import _defs, tz_utils 27 | 28 | _updated_all_tzs: list[_defs.Timezone] = [] 29 | _updated_us_tzs: list[_defs.Timezone] = [] 30 | 31 | 32 | def get_timezones( 33 | only_us: bool = False, only_fixed: bool = False 34 | ) -> list[_defs.Timezone]: 35 | """Returns an iterator of timezones. 36 | 37 | `only_us` (optional, defaults to `False`): 38 | Only return US related timezones 39 | 40 | `only_fixed` (optional, defaults to `False`): 41 | Only return fixed timezones 42 | """ 43 | global _updated_all_tzs, _updated_us_tzs 44 | 45 | # We need to update the offsets to ensure they are correct 46 | # with zoneinfo latest info 47 | if not _updated_us_tzs: 48 | _updated_us_tzs = _update_offsets(_defs._US_TIMEZONES) 49 | if not _updated_all_tzs: 50 | _updated_all_tzs = _update_offsets(_defs._ALL_TIMEZONES) 51 | 52 | if only_us: 53 | return _updated_us_tzs 54 | elif only_fixed: 55 | return _defs._FIXED_OFFSETS 56 | else: 57 | return _updated_all_tzs 58 | 59 | 60 | def get_timezones_dict() -> dict[str, _defs.Timezone]: 61 | global _ALL_TIMEZONES_DICT 62 | if _ALL_TIMEZONES_DICT is None: 63 | _ALL_TIMEZONES_DICT = {tz[1]: tz for tz in get_timezones()} 64 | return _ALL_TIMEZONES_DICT 65 | 66 | 67 | _ALL_TIMEZONES_DICT: dict[str, _defs.Timezone] | None = None 68 | 69 | 70 | def _tz_offset_key(offset) -> int: 71 | # Convert a tz offset to a key that can be used by sort(). 72 | # Just convert it to an int: +0100 -> 100, +0130 -> 130, -0345 -> -345... 73 | # that's enough for sorting. 74 | if isinstance(offset, tuple): 75 | offset = offset[0] 76 | return int(offset) 77 | 78 | 79 | def _update_offsets(timezone_collection: list[tuple[str, str]]) -> list[_defs.Timezone]: 80 | new_collection = [] 81 | 82 | for name, tz_formatted in timezone_collection: 83 | new_collection.append(tz_utils.format_tz_by_name(name, tz_formatted)) 84 | 85 | return sorted(new_collection, key=_tz_offset_key) 86 | -------------------------------------------------------------------------------- /timezones/test_timezones.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from . import _defs, tz_rendering, tz_utils, zones 4 | 5 | 6 | def assert_is_lower(offset_a, offset_b): 7 | # Force a real sort to happen: to avoid optimizations from kicking in, make 8 | # sure the list is really out of the expected order, and not in reverse 9 | # order either. 10 | coll = ["+9999", offset_b, "-9999", offset_a] 11 | coll.sort(key=zones._tz_offset_key) 12 | assert coll[1] == offset_a 13 | 14 | 15 | def test_sort(): 16 | assert_is_lower("+0100", "+0400") 17 | assert_is_lower("+0400", "+0430") 18 | assert_is_lower("+0430", "+0500") 19 | 20 | assert_is_lower("-1000", "+0430") 21 | assert_is_lower("-1030", "-0500") 22 | 23 | assert_is_lower("-1030", "+0000") 24 | 25 | assert_is_lower("-0030", "+0030") 26 | 27 | assert_is_lower("-0045", "-0030") 28 | assert_is_lower("+0030", "+0045") 29 | 30 | 31 | def test_get_timezone(): 32 | assert tz_utils.get_timezone("Europe/Moscow") is not None 33 | assert tz_utils.get_timezone("Europe/Moscow1") is None 34 | assert tz_utils.get_timezone("GMT +1:00") is not None 35 | 36 | assert tz_utils.is_valid_timezone("GMT +1:00") 37 | assert tz_utils.is_valid_timezone("Europe/Moscow") 38 | assert tz_utils.is_valid_timezone("Europe/Moscow1") is False 39 | 40 | 41 | def test_get_timezones(): 42 | assert len(list(zones.get_timezones(only_us=True))) == 8 43 | 44 | 45 | @pytest.mark.parametrize("offset_str,tzname,verbose_name", zones.get_timezones()) 46 | def test_valid_offset(offset_str, tzname, verbose_name): 47 | assert tz_utils.is_valid_timezone(tzname) 48 | tz = tz_utils.get_timezone(tzname) 49 | assert tz 50 | 51 | # 1. Find a timestamp without DST shift 52 | dt = tz_utils.get_last_datetime_without_dst(tz) 53 | 54 | # 2. Take tz shift without DST 55 | offset_full_minutes = int(tz.utcoffset(dt).total_seconds() / 60) 56 | offset_sign = "+" if offset_full_minutes >= 0 else "-" 57 | 58 | offset_hours = abs(offset_full_minutes) // 60 59 | offset_minutes = abs(offset_full_minutes) - (offset_hours * 60) 60 | expected_offset = "%s%02d%02d" % (offset_sign, offset_hours, offset_minutes) 61 | assert offset_str == expected_offset, f"Invalid offset for {tzname}" 62 | 63 | # 3. Test verbose name 64 | assert verbose_name.startswith(f"(GMT{expected_offset}) ") 65 | 66 | 67 | def test_get_timezones_json(): 68 | json_list = tz_rendering.get_timezones_json() 69 | assert "US/" in json_list 70 | 71 | 72 | @pytest.mark.parametrize("tzname", _defs._TZ_ALIASES.keys()) 73 | def test_aliases(tzname): 74 | _, name, formatted = tz_utils.format_tz_by_name(tzname) 75 | assert name == tzname 76 | assert tzname in formatted 77 | -------------------------------------------------------------------------------- /timezones/tz_rendering.py: -------------------------------------------------------------------------------- 1 | """ 2 | tz_rendering 3 | ~~~~~~~~ 4 | 5 | HTML helper to render timezones. The output will be a SELECT element. 6 | 7 | Will auto-select the current selected timezone. 8 | 9 | Example usage (returns HTML based on current properties):: 10 | 11 | html_timezones = tz_rendering.html_render_timezones( 12 | 'timezone', 13 | current_selected, 14 | first_entry=_('Select your timezone'), 15 | ) 16 | 17 | :copyright: 2012 by Amir Salihefendic ( http://amix.dk/ ) 18 | :license: MIT 19 | """ 20 | 21 | from __future__ import annotations 22 | 23 | import json 24 | from typing import Any 25 | 26 | from . import _defs, tz_utils, zones 27 | 28 | 29 | def html_render_timezones( 30 | select_name: str, 31 | current_selected: str | None = None, 32 | first_entry: str = "Select your timezone", 33 | force_current_selected: bool = False, 34 | select_id: Any = None, 35 | default_timezone: str | None = None, 36 | ) -> str: 37 | """Render timezone and output HTML. 38 | 39 | `select_name`: 40 | Is the name of the select element, e.g. . 54 | """ 55 | 56 | # Makes it possible to only mark one timezone as selected 57 | sel_checker = {"non_selected_yet": True} 58 | 59 | def render_option(value, name, selected=False): 60 | if selected and sel_checker["non_selected_yet"]: 61 | is_selected = 'selected="selected"' 62 | sel_checker["non_selected_yet"] = False 63 | else: 64 | is_selected = "" 65 | return f'' 66 | 67 | def render_option_disabled(): 68 | return '' 69 | 70 | if select_id: 71 | select_elm = f'' 74 | 75 | result = [select_elm] 76 | 77 | if first_entry: 78 | result.append(f'') 79 | result.append(render_option_disabled()) 80 | 81 | if force_current_selected and current_selected: 82 | timezone = format_tz(current_selected) 83 | if timezone: 84 | result.append(render_option(timezone[1], timezone[2], True)) 85 | result.append(render_option_disabled()) 86 | 87 | for tz in zones.get_timezones(only_us=True): 88 | result.append(render_option(tz[1], tz[2], current_selected == tz[1])) 89 | 90 | result.append(render_option_disabled()) 91 | 92 | for tz in zones.get_timezones(): 93 | result.append(render_option(tz[1], tz[2], current_selected == tz[1])) 94 | 95 | result.append(render_option_disabled()) 96 | 97 | for tz in zones.get_timezones(only_fixed=True): 98 | result.append(render_option(tz[1], tz[2], current_selected == tz[1])) 99 | 100 | result.append("") 101 | 102 | return "\n".join(result) 103 | 104 | 105 | def get_timezones_json() -> str: 106 | result = [] 107 | for tz in zones.get_timezones(only_us=True): 108 | result.append((tz[1], tz[2])) 109 | 110 | for tz in zones.get_timezones(): 111 | result.append((tz[1], tz[2])) 112 | 113 | for tz in zones.get_timezones(only_fixed=True): 114 | result.append((tz[1], tz[2])) 115 | 116 | return json.dumps(result) 117 | 118 | 119 | def format_tz(tz_name: str) -> _defs.Timezone: 120 | tz = zones.get_timezones_dict().get(tz_name) 121 | if tz: 122 | return tz 123 | return tz_utils.format_tz_by_name(tz_name) 124 | -------------------------------------------------------------------------------- /timezones/tz_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | tz_utils 3 | ~~~~~~~~ 4 | 5 | Includes timezone related utilities. 6 | 7 | 8 | Example usage (get a fixed offset timezone):: 9 | 10 | print tz_utils.get_timezone('GMT +10:00') 11 | 12 | 13 | Example usage (format timezone by name):: 14 | 15 | print tz_utils.format_tz_by_name('Europe/Copenhagen') 16 | => 17 | ("+0100", "Europe/Copenhagen", '(GMT+0100) Copenhagen') 18 | 19 | 20 | Example usage (is a timezone valid?):: 21 | 22 | print tz_utils.is_valid_timezone('Europe/Copenhagen') 23 | => 24 | True 25 | 26 | 27 | :copyright: 2012 by Amir Salihefendic ( http://amix.dk/ ) 28 | :license: MIT 29 | """ 30 | 31 | from __future__ import annotations 32 | 33 | from datetime import datetime, timedelta, tzinfo 34 | 35 | import zoneinfo as zi 36 | 37 | from . import _defs 38 | 39 | # --- Exports ---------------------------------------------- 40 | __all__ = [ 41 | "get_timezone", 42 | "is_valid_timezone", 43 | "format_tz_by_name", 44 | ] 45 | 46 | 47 | def get_timezone(tzname: str) -> tzinfo | None: 48 | """ 49 | Get a timezone instance by name or return `None`. 50 | 51 | This getter support fixed offest timezone like `get_timezone('GMT +10:00')` 52 | """ 53 | try: 54 | # First, try with the provided name 55 | return zi.ZoneInfo(tzname) 56 | except zi.ZoneInfoNotFoundError: 57 | pass 58 | 59 | # No result: try with an alias, if there's one 60 | if alias := (_defs._TZ_ALIASES.get(tzname)): 61 | try: 62 | return zi.ZoneInfo(alias) 63 | except zi.ZoneInfoNotFoundError: 64 | pass 65 | 66 | # Still no result: fallback to a static timezone, or return None 67 | return _tz_map().get(tzname) 68 | 69 | 70 | def is_valid_timezone(timezone: str) -> bool: 71 | """Return `True` if the `timezone` is valid. Otherwise `False` is returned.""" 72 | try: 73 | tz = get_timezone(timezone) 74 | return bool(tz) 75 | except Exception: 76 | return False 77 | 78 | 79 | def format_tz_by_name(tz_name: str, tz_formatted: str | None = None) -> _defs.Timezone: 80 | """Returns a tuple of (tz_offets, tz_name, tz_formatted). 81 | 82 | >>> format_tz_by_name("Europe/Copenhagen") 83 | ("+0100", "Europe/Copenhagen", "(GMT+0100) Copenhagen") 84 | >>> format_tz_by_name("America/Sao_Paulo", "Brasilia, Sao Paulo") 85 | ("-0300", "America/Sao_Paulo", "(GMT-0300) Brasilia, Sao Paulo") 86 | """ 87 | tz = get_timezone(tz_name) 88 | if not tz: 89 | raise ValueError(f"Invalid timezone {tz_name}") 90 | 91 | # Make sure we have a date without DST 92 | dt = get_last_datetime_without_dst(tz) 93 | offset = dt.strftime("%z") 94 | 95 | tz_formatted = f"(GMT{offset}) {tz_formatted or tz_name}" 96 | return (offset, tz_name, tz_formatted) 97 | 98 | 99 | def get_last_datetime_without_dst(tz: tzinfo): 100 | dt = datetime.now(tz) 101 | while (dst := dt.dst()) is not None and dst.total_seconds() != 0: 102 | dt -= timedelta(days=30) 103 | return dt 104 | 105 | 106 | # --- Private ---------------------------------------------- 107 | _zero = timedelta(0) 108 | 109 | 110 | class FixedOffset(tzinfo): 111 | """Fixed offset in minutes east from UTC.""" 112 | 113 | def __init__(self, offset, name): 114 | self.offset = offset 115 | self.name = name 116 | 117 | self._offset = timedelta(minutes=offset) 118 | self.zone = name 119 | 120 | def __str__(self): 121 | return self.zone 122 | 123 | def utcoffset(self, dt): 124 | return self._offset 125 | 126 | def tzname(self, dt): 127 | return self.zone 128 | 129 | def dst(self, dt): 130 | return _zero 131 | 132 | def localize(self, dt, is_dst=False): 133 | """Convert naive time to local time""" 134 | if dt.tzinfo is not None: 135 | raise ValueError("Not naive datetime (tzinfo is already set)") 136 | return dt.replace(tzinfo=self) 137 | 138 | def __getinitargs__(self): 139 | return (self.offset, self.name) 140 | 141 | 142 | TZ_MAP = None 143 | 144 | 145 | def _tz_map(): 146 | global TZ_MAP 147 | 148 | if TZ_MAP is None: 149 | timezones = [ 150 | FixedOffset(-720, "GMT -12:00"), 151 | FixedOffset(-660, "GMT -11:00"), 152 | FixedOffset(-600, "GMT -10:00"), 153 | FixedOffset(-540, "GMT -9:00"), 154 | FixedOffset(-480, "GMT -8:00"), 155 | FixedOffset(-420, "GMT -7:00"), 156 | FixedOffset(-360, "GMT -6:00"), 157 | FixedOffset(-300, "GMT -5:00"), 158 | FixedOffset(-240, "GMT -4:00"), 159 | FixedOffset(-180, "GMT -3:00"), 160 | FixedOffset(-120, "GMT -2:00"), 161 | FixedOffset(-60, "GMT -1:00"), 162 | FixedOffset(0, "GMT"), 163 | FixedOffset(60, "GMT +1:00"), 164 | FixedOffset(120, "GMT +2:00"), 165 | FixedOffset(180, "GMT +3:00"), 166 | FixedOffset(240, "GMT +4:00"), 167 | FixedOffset(300, "GMT +5:00"), 168 | FixedOffset(360, "GMT +6:00"), 169 | FixedOffset(420, "GMT +7:00"), 170 | FixedOffset(480, "GMT +8:00"), 171 | FixedOffset(540, "GMT +9:00"), 172 | FixedOffset(600, "GMT +10:00"), 173 | FixedOffset(660, "GMT +11:00"), 174 | FixedOffset(720, "GMT +12:00"), 175 | FixedOffset(780, "GMT +13:00"), 176 | ] 177 | 178 | TZ_MAP = {z.zone: z for z in timezones} 179 | 180 | return TZ_MAP 181 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "timezones" 3 | version = "3.0.0" 4 | description = "A Python library that provides better selection of common timezones, can output HTML and auto select the best timezone based on user's IP." 5 | license = "MIT" 6 | keywords = ["timezones", "timezone"] 7 | classifiers = [ 8 | "Development Status :: 5 - Production/Stable", 9 | "Intended Audience :: Developers", 10 | "License :: OSI Approved :: MIT License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python", 13 | "Programming Language :: Python :: 3", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | "Programming Language :: Python :: 3.13", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | ] 19 | homepage = "https://doist.com" 20 | authors = ["Doist Developers "] 21 | readme = "README.md" 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.11" 25 | 26 | [tool.poetry.dev-dependencies] 27 | mypy = "^1.13" 28 | pytest = "^7.1.2" 29 | ruff = "^0.7.3" 30 | tox = "^3.25.0" 31 | tox-gh-actions = "^2.9.1" 32 | 33 | [build-system] 34 | requires = ["poetry-core>=1.0.0"] 35 | build-backend = "poetry.core.masonry.api" 36 | 37 | 38 | [tool.ruff] 39 | # By default, always show source code snippets. 40 | output-format = 'full' 41 | 42 | extend-exclude = [ 43 | "env", 44 | "runtime", 45 | ] 46 | 47 | [tool.ruff.lint] 48 | select = [ 49 | "ASYNC", # flake8-async 50 | "C4", # flake8-comprehensions 51 | "D", # pydocstyle, 52 | "E", "W", # pycodestyle 53 | "F", # pyflakes 54 | "I", # isort 55 | "PL", # pylint 56 | "RUF", # ruff 57 | "S", # flake8-bandit 58 | "SIM", # flake8-simplify 59 | "UP", # pyupgrade 60 | "TCH", # flake8-type-checking 61 | ] 62 | 63 | ignore = [ 64 | ## D - pydocstyle ## 65 | # D1XX errors are OK. Don't force people into over-documenting. 66 | "D100", "D101", "D102", "D103", "D104", "D105", "D107", 67 | # These need to be fixed. 68 | "D202", "D205", "D400", "D401", 69 | 70 | ## E / W - pycodestyle ## 71 | "E501", # line too long 72 | "E203", # whitespace-before-punctuation 73 | "E741", # ambiguous variable name 74 | 75 | ## PL - pylint ## 76 | # Commented-out rules are rules that we disable in pylint but are not supported by ruff yet. 77 | 78 | "PLR6301", # no-self-use 79 | "PLC2701", # import-private-name 80 | 81 | # Import order issues 82 | # "PLC0411", # wrong-import-order 83 | # "PLC0412", # wrong-import-position 84 | "PLC0414", # ungrouped-imports 85 | "PLC0415", # import-outside-top-level 86 | 87 | # Documentation issues 88 | # "C0114", # missing-module-docstring 89 | 90 | # Complexity issues 91 | "PLR0904", # too-many-public-methods 92 | # "PLC0302", # too-many-lines 93 | "PLR1702", # too-many-nested-blocks 94 | # "PLR0902", # too-many-instance-attributes 95 | "PLR0911", # too-many-return-statements 96 | "PLR0915", # too-many-statements 97 | "PLR0912", # too-many-branches 98 | # "PLR0903", # too-few-public-methods 99 | "PLR0914", # too-many-locals 100 | # "PLC0301", # line-too-long 101 | "PLR0913", # too-many-arguments 102 | "PLR0917", # too-many-positional 103 | "PLR2004", # magic-value-comparison 104 | "PLR5501", # collapsible-else-if 105 | "PLW0603", # global-statement 106 | "PLW2901", # redefined-loop-name 107 | "PLC1901", # compare-to-empty-string 108 | 109 | ## RUF - ruff ## 110 | "RUF001", # ambiguous-unicode-character-string 111 | "RUF002", # ambiguous-unicode-character-docstring 112 | "RUF003", # ambiguous-unicode-character-comment 113 | "RUF012", # mutable-class-default 114 | "RUF018", # assignment-in-assert 115 | 116 | # Enable when Poetry supports PEP 621 and we migrate our confguration to it. 117 | # See: https://github.com/python-poetry/poetry-core/pull/567 118 | "RUF200", 119 | 120 | "S101", # assert 121 | "S104", # hardcoded-bind-all-interfaces 122 | "S105", # hardcoded-password-string 123 | "S106", # hardcoded-password-func-arg 124 | "S107", # hardcoded-password-default 125 | "S110", # try-except-pass 126 | "S301", # suspicious-pickle-usage 127 | "S303", # suspicious-insecure-hash-usage 128 | "S310", # suspicious-url-open-usage 129 | "S311", # suspicious-non-cryptographic-random-usage 130 | "S324", # hashlib-insecure-hash-function 131 | "S603", # subprocess-without-shell-equals-true 132 | "S607", # start-process-with-partial-path 133 | "S608", # hardcoded-sql-expression 134 | 135 | ## SIM - flake8-simplify ## 136 | "SIM102", # collapsible-if 137 | "SIM105", # suppressible-exception 138 | "SIM108", # if-else-block-instead-of-if-exp 139 | "SIM114", # if-with-same-arms 140 | "SIM116", # if-else-block-instead-of-dict-lookup 141 | "SIM117", # multiple-with-statements 142 | 143 | # Enable when the rule is out of preview and false-positives are handled. 144 | # See: https://docs.astral.sh/ruff/rules/in-dict-keys/ 145 | "SIM118", # in-dict-keys 146 | 147 | ## TCH - flake8-type-checking ## 148 | "TCH001", # typing-only-first-party-import 149 | "TCH002", # typing-only-third-party-import 150 | "TCH003", # typing-only-standard-library-import 151 | ] 152 | 153 | [tool.ruff.lint.isort] 154 | section-order = [ 155 | "future", 156 | "standard-library", 157 | "third-party", 158 | "first-party", 159 | "local-folder", 160 | ] 161 | 162 | [tool.ruff.lint.pydocstyle] 163 | convention = "pep257" 164 | 165 | [tool.ruff.lint.pyupgrade] 166 | # Required by tools like Pydantic that use type information at runtime. 167 | # https://github.com/asottile/pyupgrade/issues/622#issuecomment-1088766572 168 | keep-runtime-typing = true 169 | 170 | [tool.ruff.format] 171 | docstring-code-format = true 172 | -------------------------------------------------------------------------------- /timezones/_defs.py: -------------------------------------------------------------------------------- 1 | Timezone = tuple[ 2 | str, # offset 3 | str, # timezone name 4 | str, # formatted name 5 | ] 6 | 7 | _US_TIMEZONES = [ 8 | ("US/Hawaii", "Hawaii"), 9 | ("US/Alaska", "Alaska"), 10 | ("US/Pacific", "Pacific Time (US & Canada)"), 11 | ("US/Arizona", "Arizona"), 12 | ("US/Mountain", "Mountain Time (US & Canada)"), 13 | ("US/Central", "Central Time (US & Canada)"), 14 | ("US/Eastern", "Eastern Time (US & Canada)"), 15 | ("US/East-Indiana", "Indiana (East)"), 16 | ] 17 | 18 | _ALL_TIMEZONES = [ 19 | # -11 20 | ("Pacific/Midway", "International Date Line West"), 21 | ("Pacific/Midway", "Midway Island"), 22 | ("Pacific/Samoa", "Samoa"), 23 | # -10 24 | ("US/Hawaii", "Hawaii"), 25 | # -09 26 | ("US/Alaska", "Alaska"), 27 | # -08 28 | ("US/Pacific", "Pacific Time (US & Canada)"), 29 | ("America/Tijuana", "Tijuana"), 30 | # -07 31 | ("US/Arizona", "Arizona"), 32 | ("America/Mazatlan", "Mazatlan"), 33 | ("US/Mountain", "Mountain Time (US & Canada)"), 34 | # -06 35 | ("America/Chihuahua", "Chihuahua"), 36 | ("US/Central", "Central Time (US & Canada)"), 37 | ("Canada/Central", "Central America"), 38 | ("Canada/Central", "Central Time (US & Canada)"), 39 | ("Mexico/General", "Guadalajara"), 40 | ("Mexico/General", "Mexico City"), 41 | ("America/Monterrey", "Monterrey"), 42 | ("Canada/Saskatchewan", "Saskatchewan"), 43 | # -05 44 | ("America/Bogota", "Bogota"), 45 | ("US/Eastern", "Eastern Time (US & Canada)"), 46 | ("US/East-Indiana", "Indiana (East)"), 47 | ("America/Lima", "Lima"), 48 | ("America/Rio_Branco", "Rio Branco"), 49 | ("Etc/GMT+5", "Quito"), # "plus" value is correct! 50 | # -04 51 | ("America/Caracas", "Caracas"), 52 | ("Canada/Atlantic", "Atlantic Time (Canada)"), 53 | ("Etc/GMT+4", "La Paz"), # correct as well 54 | ("America/Cuiaba", "Cuiaba"), 55 | ("America/Manaus", "Manaus"), 56 | ("America/Santiago", "Santiago"), 57 | ("America/Cuiaba", "Mato Grosso"), 58 | ("America/Guyana", "Georgetown"), 59 | # -03 60 | ("Canada/Newfoundland", "Newfoundland"), 61 | ("America/Argentina/Buenos_Aires", "Buenos Aires"), 62 | ("America/Godthab", "Greenland"), 63 | ("America/Fortaleza", "NE Brazil, Fortaleza"), 64 | ("America/Sao_Paulo", "Brasilia, Sao Paulo"), 65 | # -02 66 | ("America/Noronha", "Fernando de Noronha"), 67 | # -01 68 | ("Atlantic/Azores", "Azores"), 69 | ("Atlantic/Cape_Verde", "Cape Verde Is. "), 70 | # +00 71 | ("Africa/Casablanca", "Casablanca"), 72 | ("Europe/Dublin", "Dublin"), 73 | ("Europe/London", "Edinburgh"), 74 | ("Europe/Lisbon", "Lisbon"), 75 | ("Europe/London", "London"), 76 | ("Africa/Monrovia", "Monrovia"), 77 | ("UTC", "UTC"), 78 | # +01 79 | ("Europe/Amsterdam", "Amsterdam"), 80 | ("Europe/Belgrade", "Belgrade"), 81 | ("Europe/Berlin", "Berlin"), 82 | ("Europe/Zurich", "Bern"), 83 | ("Europe/Bratislava", "Bratislava"), 84 | ("Europe/Brussels", "Brussels"), 85 | ("Europe/Budapest", "Budapest"), 86 | ("Europe/Copenhagen", "Copenhagen"), 87 | ("Europe/Ljubljana", "Ljubljana"), 88 | ("Europe/Madrid", "Madrid"), 89 | ("Europe/Oslo", "Oslo"), 90 | ("Europe/Paris", "Paris"), 91 | ("Europe/Prague", "Prague"), 92 | ("Europe/Rome", "Rome"), 93 | ("Europe/Sarajevo", "Sarajevo"), 94 | ("Europe/Skopje", "Skopje"), 95 | ("Europe/Stockholm", "Stockholm"), 96 | ("Europe/Vienna", "Vienna"), 97 | ("Europe/Warsaw", "Warsaw"), 98 | ("Europe/Zagreb", "Zagreb"), 99 | # +02 100 | ("Europe/Athens", "Athens"), 101 | ("Europe/Bucharest", "Bucharest"), 102 | ("Africa/Cairo", "Cairo"), 103 | ("Africa/Harare", "Harare"), 104 | ("Europe/Helsinki", "Helsinki"), 105 | ("Asia/Jerusalem", "Jerusalem"), 106 | ("Europe/Kyiv", "Kyiv"), 107 | ("Africa/Johannesburg", "Pretoria"), 108 | ("Europe/Riga", "Riga"), 109 | ("Europe/Sofia", "Sofia"), 110 | ("Europe/Tallinn", "Tallinn"), 111 | ("Europe/Vilnius", "Vilnius"), 112 | # +03 113 | ("Asia/Baghdad", "Baghdad"), 114 | ("Asia/Kuwait", "Kuwait"), 115 | ("Europe/Istanbul", "Istanbul"), 116 | ("Europe/Minsk", "Minsk"), 117 | ("Europe/Moscow", "Moscow"), 118 | ("Africa/Nairobi", "Nairobi"), 119 | ("Asia/Riyadh", "Riyadh"), 120 | ("Europe/Moscow", "St. Petersburg"), 121 | ("Europe/Volgograd", "Volgograd"), 122 | ("Asia/Tehran", "Tehran"), 123 | # +04 124 | ("Asia/Dubai", "Abu Dhabi"), 125 | ("Asia/Baku", "Baku"), 126 | ("Asia/Muscat", "Muscat"), 127 | ("Asia/Tbilisi", "Tbilisi"), 128 | ("Asia/Yerevan", "Yerevan"), 129 | ("Asia/Kabul", "Kabul"), 130 | # +05 131 | ("Asia/Karachi", "Islamabad"), 132 | ("Asia/Karachi", "Karachi"), 133 | ("Asia/Tashkent", "Tashkent"), 134 | # Note that different locations match the same timezone name 135 | # and the location which gives the name to the timezone 136 | # comes last. It's to ensure that the function 137 | # html_render_timezones(..., current_selected='Asia/Calcutta') 138 | # takes "most sensible" timezone name. 139 | ("Asia/Calcutta", "Chennai"), 140 | ("Asia/Calcutta", "Mumbai"), 141 | ("Asia/Calcutta", "New Delhi"), 142 | ("Asia/Calcutta", "Sri Jayawardenepura"), 143 | ("Asia/Calcutta", "Kolkata"), 144 | ("Asia/Kathmandu", "Kathmandu"), 145 | # +06 146 | ("Asia/Almaty", "Almaty"), 147 | ("Asia/Almaty", "Astana"), 148 | ("Asia/Dhaka", "Dhaka"), 149 | ("Asia/Urumqi", "Urumqi"), 150 | ("Asia/Rangoon", "Rangoon"), 151 | # +07 152 | ("Asia/Novosibirsk", "Novosibirsk"), 153 | ("Asia/Bangkok", "Bangkok"), 154 | ("Asia/Saigon", "Hanoi"), 155 | ("Asia/Jakarta", "Jakarta"), 156 | ("Asia/Krasnoyarsk", "Krasnoyarsk"), 157 | # +08 158 | ("Asia/Harbin", "Beijing"), 159 | ("Asia/Chongqing", "Chongqing"), 160 | ("Asia/Hong_Kong", "Hong Kong"), 161 | ("Asia/Irkutsk", "Irkutsk"), 162 | ("Asia/Kuala_Lumpur", "Kuala Lumpur"), 163 | ("Australia/Perth", "Perth"), 164 | ("Singapore", "Singapore"), 165 | ("Asia/Ulaanbaatar", "Ulaanbaatar"), 166 | ("Asia/Taipei", "Taipei"), 167 | # +09 168 | ("Asia/Seoul", "Seoul"), 169 | ("Asia/Tokyo", "Tokyo"), 170 | ("Asia/Yakutsk", "Yakutsk"), 171 | ("Australia/Adelaide", "Adelaide"), 172 | ("Australia/Darwin", "Darwin"), 173 | # +10 174 | ("Australia/Brisbane", "Brisbane"), 175 | ("Australia/Canberra", "Canberra"), 176 | ("Pacific/Guam", "Guam"), 177 | ("Australia/Hobart", "Hobart"), 178 | ("Australia/Melbourne", "Melbourne"), 179 | ("Pacific/Port_Moresby", "Port Moresby"), 180 | ("Australia/Sydney", "Sydney"), 181 | ("Asia/Vladivostok", "Vladivostok"), 182 | # +11 183 | ("Asia/Magadan", "Magadan"), 184 | ("Pacific/Noumea", "New Caledonia"), 185 | ("Pacific/Guadalcanal", "Solomon Is. "), 186 | ("Pacific/Norfolk", "Norfolk"), 187 | # +12 188 | ("Pacific/Auckland", "Auckland"), 189 | ("Pacific/Fiji", "Fiji"), 190 | ("Asia/Kamchatka", "Kamchatka"), 191 | ("Asia/Kamchatka", "Marshall Is."), 192 | ("Pacific/Auckland", "Wellington"), 193 | # +13 194 | ("Pacific/Tongatapu", "Nuku'alofa"), 195 | ] 196 | 197 | _TZ_ALIASES = { 198 | "Europe/Kyiv": "Europe/Kiev", 199 | } 200 | 201 | _FIXED_OFFSETS: list[Timezone] = [ 202 | ("-1200", "GMT -12:00", "GMT -12:00"), 203 | ("-1100", "GMT -11:00", "GMT -11:00"), 204 | ("-1000", "GMT -10:00", "GMT -10:00"), 205 | ("-0900", "GMT -9:00", "GMT -9:00"), 206 | ("-0800", "GMT -8:00", "GMT -8:00"), 207 | ("-0700", "GMT -7:00", "GMT -7:00"), 208 | ("-0600", "GMT -6:00", "GMT -6:00"), 209 | ("-0500", "GMT -5:00", "GMT -5:00"), 210 | ("-0400", "GMT -4:00", "GMT -4:00"), 211 | ("-0300", "GMT -3:00", "GMT -3:00"), 212 | ("-0200", "GMT -2:00", "GMT -2:00"), 213 | ("-0100", "GMT -1:00", "GMT -1:00"), 214 | ("+0000", "GMT", "GMT"), 215 | ("+0000", "UTC", "UTC"), 216 | ("+0100", "GMT +1:00", "GMT +1:00"), 217 | ("+0200", "GMT +2:00", "GMT +2:00"), 218 | ("+0300", "GMT +3:00", "GMT +3:00"), 219 | ("+0400", "GMT +4:00", "GMT +4:00"), 220 | ("+0500", "GMT +5:00", "GMT +5:00"), 221 | ("+0600", "GMT +6:00", "GMT +6:00"), 222 | ("+0700", "GMT +7:00", "GMT +7:00"), 223 | ("+0800", "GMT +8:00", "GMT +8:00"), 224 | ("+0900", "GMT +9:00", "GMT +9:00"), 225 | ("+1000", "GMT +10:00", "GMT +10:00"), 226 | ("+1100", "GMT +11:00", "GMT +11:00"), 227 | ("+1200", "GMT +12:00", "GMT +12:00"), 228 | ("+1300", "GMT +13:00", "GMT +13:00"), 229 | ] 230 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "colorama" 5 | version = "0.4.6" 6 | description = "Cross-platform colored terminal text." 7 | optional = false 8 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 9 | groups = ["dev"] 10 | markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" 11 | files = [ 12 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 13 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 14 | ] 15 | 16 | [[package]] 17 | name = "distlib" 18 | version = "0.3.9" 19 | description = "Distribution utilities" 20 | optional = false 21 | python-versions = "*" 22 | groups = ["dev"] 23 | files = [ 24 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 25 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 26 | ] 27 | 28 | [[package]] 29 | name = "filelock" 30 | version = "3.20.1" 31 | description = "A platform independent file lock." 32 | optional = false 33 | python-versions = ">=3.10" 34 | groups = ["dev"] 35 | files = [ 36 | {file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"}, 37 | {file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"}, 38 | ] 39 | 40 | [[package]] 41 | name = "importlib-resources" 42 | version = "6.4.5" 43 | description = "Read resources from Python packages" 44 | optional = false 45 | python-versions = ">=3.8" 46 | groups = ["dev"] 47 | files = [ 48 | {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, 49 | {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, 50 | ] 51 | 52 | [package.extras] 53 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 54 | cover = ["pytest-cov"] 55 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 56 | enabler = ["pytest-enabler (>=2.2)"] 57 | test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] 58 | type = ["pytest-mypy"] 59 | 60 | [[package]] 61 | name = "iniconfig" 62 | version = "2.0.0" 63 | description = "brain-dead simple config-ini parsing" 64 | optional = false 65 | python-versions = ">=3.7" 66 | groups = ["dev"] 67 | files = [ 68 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 69 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 70 | ] 71 | 72 | [[package]] 73 | name = "mypy" 74 | version = "1.13.0" 75 | description = "Optional static typing for Python" 76 | optional = false 77 | python-versions = ">=3.8" 78 | groups = ["dev"] 79 | files = [ 80 | {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, 81 | {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, 82 | {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, 83 | {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, 84 | {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, 85 | {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, 86 | {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, 87 | {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, 88 | {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, 89 | {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, 90 | {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, 91 | {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, 92 | {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, 93 | {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, 94 | {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, 95 | {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, 96 | {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, 97 | {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, 98 | {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, 99 | {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, 100 | {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, 101 | {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, 102 | {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, 103 | {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, 104 | {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, 105 | {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, 106 | {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, 107 | {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, 108 | {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, 109 | {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, 110 | {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, 111 | {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, 112 | ] 113 | 114 | [package.dependencies] 115 | mypy-extensions = ">=1.0.0" 116 | typing-extensions = ">=4.6.0" 117 | 118 | [package.extras] 119 | dmypy = ["psutil (>=4.0)"] 120 | faster-cache = ["orjson"] 121 | install-types = ["pip"] 122 | mypyc = ["setuptools (>=50)"] 123 | reports = ["lxml"] 124 | 125 | [[package]] 126 | name = "mypy-extensions" 127 | version = "1.0.0" 128 | description = "Type system extensions for programs checked with the mypy type checker." 129 | optional = false 130 | python-versions = ">=3.5" 131 | groups = ["dev"] 132 | files = [ 133 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 134 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 135 | ] 136 | 137 | [[package]] 138 | name = "packaging" 139 | version = "24.2" 140 | description = "Core utilities for Python packages" 141 | optional = false 142 | python-versions = ">=3.8" 143 | groups = ["dev"] 144 | files = [ 145 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 146 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 147 | ] 148 | 149 | [[package]] 150 | name = "platformdirs" 151 | version = "4.3.6" 152 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 153 | optional = false 154 | python-versions = ">=3.8" 155 | groups = ["dev"] 156 | files = [ 157 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 158 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 159 | ] 160 | 161 | [package.extras] 162 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 163 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 164 | type = ["mypy (>=1.11.2)"] 165 | 166 | [[package]] 167 | name = "pluggy" 168 | version = "1.5.0" 169 | description = "plugin and hook calling mechanisms for python" 170 | optional = false 171 | python-versions = ">=3.8" 172 | groups = ["dev"] 173 | files = [ 174 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 175 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 176 | ] 177 | 178 | [package.extras] 179 | dev = ["pre-commit", "tox"] 180 | testing = ["pytest", "pytest-benchmark"] 181 | 182 | [[package]] 183 | name = "py" 184 | version = "1.11.0" 185 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 186 | optional = false 187 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 188 | groups = ["dev"] 189 | files = [ 190 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 191 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 192 | ] 193 | 194 | [[package]] 195 | name = "pytest" 196 | version = "7.4.4" 197 | description = "pytest: simple powerful testing with Python" 198 | optional = false 199 | python-versions = ">=3.7" 200 | groups = ["dev"] 201 | files = [ 202 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 203 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 204 | ] 205 | 206 | [package.dependencies] 207 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 208 | iniconfig = "*" 209 | packaging = "*" 210 | pluggy = ">=0.12,<2.0" 211 | 212 | [package.extras] 213 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 214 | 215 | [[package]] 216 | name = "ruff" 217 | version = "0.7.3" 218 | description = "An extremely fast Python linter and code formatter, written in Rust." 219 | optional = false 220 | python-versions = ">=3.7" 221 | groups = ["dev"] 222 | files = [ 223 | {file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"}, 224 | {file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"}, 225 | {file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"}, 226 | {file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"}, 227 | {file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"}, 228 | {file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"}, 229 | {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"}, 230 | {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"}, 231 | {file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"}, 232 | {file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"}, 233 | {file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"}, 234 | {file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"}, 235 | {file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"}, 236 | {file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"}, 237 | {file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"}, 238 | {file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"}, 239 | {file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"}, 240 | {file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"}, 241 | ] 242 | 243 | [[package]] 244 | name = "six" 245 | version = "1.16.0" 246 | description = "Python 2 and 3 compatibility utilities" 247 | optional = false 248 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 249 | groups = ["dev"] 250 | files = [ 251 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 252 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 253 | ] 254 | 255 | [[package]] 256 | name = "tox" 257 | version = "3.28.0" 258 | description = "tox is a generic virtualenv management and test command line tool" 259 | optional = false 260 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 261 | groups = ["dev"] 262 | files = [ 263 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 264 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 265 | ] 266 | 267 | [package.dependencies] 268 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 269 | filelock = ">=3.0.0" 270 | packaging = ">=14" 271 | pluggy = ">=0.12.0" 272 | py = ">=1.4.17" 273 | six = ">=1.14.0" 274 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 275 | 276 | [package.extras] 277 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 278 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3) ; python_version < \"3.4\"", "psutil (>=5.6.1) ; platform_python_implementation == \"cpython\"", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] 279 | 280 | [[package]] 281 | name = "tox-gh-actions" 282 | version = "2.12.0" 283 | description = "Seamless integration of tox into GitHub Actions" 284 | optional = false 285 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 286 | groups = ["dev"] 287 | files = [ 288 | {file = "tox-gh-actions-2.12.0.tar.gz", hash = "sha256:7a8aa62cd616b0e74c7db204bc44bbd603574f468f00c4ba3a2a3c87de8cf514"}, 289 | {file = "tox_gh_actions-2.12.0-py2.py3-none-any.whl", hash = "sha256:5214db422a3297854db14fe814d59bd95674b7c577793bf406e7832dabeca03d"}, 290 | ] 291 | 292 | [package.dependencies] 293 | importlib-resources = "*" 294 | tox = ">=3.12,<4" 295 | 296 | [package.extras] 297 | testing = ["black ; platform_python_implementation == \"CPython\" and python_version >= \"3.6\"", "coverage (<6)", "flake8 (>=3,<4)", "pytest (>=4,<7) ; python_version < \"3.10\"", "pytest (>=6.2.5,<7) ; python_version >= \"3.10\"", "pytest-cov (>=2,<3)", "pytest-mock (>=2,<3)", "pytest-randomly (>=3) ; python_version >= \"3.5\""] 298 | 299 | [[package]] 300 | name = "typing-extensions" 301 | version = "4.12.2" 302 | description = "Backported and Experimental Type Hints for Python 3.8+" 303 | optional = false 304 | python-versions = ">=3.8" 305 | groups = ["dev"] 306 | files = [ 307 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 308 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 309 | ] 310 | 311 | [[package]] 312 | name = "virtualenv" 313 | version = "20.27.1" 314 | description = "Virtual Python Environment builder" 315 | optional = false 316 | python-versions = ">=3.8" 317 | groups = ["dev"] 318 | files = [ 319 | {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, 320 | {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, 321 | ] 322 | 323 | [package.dependencies] 324 | distlib = ">=0.3.7,<1" 325 | filelock = ">=3.12.2,<4" 326 | platformdirs = ">=3.9.1,<5" 327 | 328 | [package.extras] 329 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 330 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 331 | 332 | [metadata] 333 | lock-version = "2.1" 334 | python-versions = "^3.11" 335 | content-hash = "aa9494d88860b79b8b586aa88b7bf1a389689e6b64fb40e02f8b4b03b3212405" 336 | --------------------------------------------------------------------------------