diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 483ecdf..b907517 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,7 +67,7 @@ jobs: run: make mypy - name: Test - run: python -mcoverage run --branch -m unittest testleapseconddata.py && python -mcoverage report --fail-under=100 && python -mcoverage xml + run: python -X tracemalloc=3 -mcoverage run --branch -m unittest testleapseconddata.py && python -mcoverage report --fail-under=100 && python -mcoverage xml pre-commit: runs-on: ubuntu-latest diff --git a/docs/conf.py b/docs/conf.py index e50db8d..8d93ead 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '_env'] # -- Options for HTML output ------------------------------------------------- diff --git a/leapseconddata/__init__.py b/leapseconddata/__init__.py index 60c0d18..4ebb7a5 100755 --- a/leapseconddata/__init__.py +++ b/leapseconddata/__init__.py @@ -30,7 +30,7 @@ import re import urllib.request from dataclasses import dataclass, field -from typing import BinaryIO +from typing import BinaryIO, ClassVar tai = datetime.timezone(datetime.timedelta(0), "TAI") @@ -76,6 +76,49 @@ class LeapSecondData: :param Optional[datetime.datetime] updated: The last update time of the data """ + standard_file_sources: ClassVar[list[str]] = [ + "file:///usr/share/zoneinfo/leap-seconds.list", # Debian Linux + "file:///var/db/ntpd.leap-seconds.list", # FreeBSD + ] + """When using `LeapSecondData.from_standard_source`, these local sources are checked first. + + Locations for Debian Linux & FreeBSD are supported.""" + + standard_network_sources: ClassVar[list[str]] = [ + "https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list", + "https://data.iana.org/time-zones/tzdb/leap-seconds.list", + "https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list", + "ftp://ftp.boulder.nist.gov/pub/time/leap-seconds.list", + "https://www.meinberg.de/download/ntp/leap-seconds.list", + ] + """When using `LeapSecondData.from_standard_source`, these network sources are checked second. + + Remote sources are checked in the following order until a suitable file is found: + + * The `International Earth Rotation Service (IERS) + `_ is the international + body charged with various duties including scheduling leap seconds. + * The `Internet Assigned Numbers Authority (IANA) + `_ publishes the IANA timezone database, used by + many major operating sytsems for handling the world's time zones. As part + of this activity they publish a version of the leap second list. + * `eggert/tz `_ is the canonical github home + of the IANA timezone database, and updated versions of the leap second + list can appear here before they are part of an official IANA timezone + database release. + * `The National Institute of Standards and Technology (NIST)'s Time + Realization and Distribution Group + `_ + is a US federal organization that publishes a version of the leap second + database. + * `Meinberg Funkuhren GmbH & Co. KG + `_ is a Germany-based + company that published a `helpful article in its knowledge base + `_ + including URLs of sites that disseminate the leap second list. They state + that the version they distribute is frequently more up to date than other + sources, including IANA, NIST, and tzdb.""" + leap_seconds: list[LeapSecondInfo] """All known and scheduled leap seconds""" @@ -204,12 +247,7 @@ def from_standard_source( leap-second.list data valid for the given timestamp, or the current time (if unspecified) """ - for location in [ # pragma no branch - "file:///usr/share/zoneinfo/leap-seconds.list", # Debian Linux - "file:///var/db/ntpd.leap-seconds.list", # FreeBSD - "https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list", - "https://www.meinberg.de/download/ntp/leap-seconds.list", - ]: + for location in cls.standard_file_sources + cls.standard_network_sources: logging.debug("Trying leap second data from %s", location) try: candidate = cls.from_url(location, check_hash=check_hash) @@ -244,14 +282,13 @@ def from_file( @classmethod def from_url( cls, - url: str = "https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list", + url: str, *, check_hash: bool = True, ) -> LeapSecondData | None: """Retrieve the leap second list from a local file - :param filename: URL to read leap second data from. The - default is maintained by the tzdata authors + :param filename: URL to read leap second data from :param check_hash: Whether to check the embedded hash """ try: diff --git a/leapseconddata/__main__.py b/leapseconddata/__main__.py index 44476ea..42674a7 100644 --- a/leapseconddata/__main__.py +++ b/leapseconddata/__main__.py @@ -13,7 +13,7 @@ import click -from . import LeapSecondData, tai +from . import InvalidHashError, LeapSecondData, tai utc = datetime.timezone.utc @@ -165,5 +165,29 @@ def table(ctx: click.Context, *, start: datetime.datetime, end: datetime.datetim print(f"{leap_second.start:%Y-%m-%d}: {leap_second.tai_offset.total_seconds():.0f}") +@cli.command +def sources() -> None: + """Print information about leap-second.list data sources""" + first = True + for location in LeapSecondData.standard_file_sources + LeapSecondData.standard_network_sources: + if not first: + print() + first = False + try: + leap_second_data = LeapSecondData.from_url(location, check_hash=True) + except InvalidHashError: # pragma no coverage + print(f"{location}: Invalid hash") + leap_second_data = LeapSecondData.from_url(location, check_hash=False) + except Exception as e: # pragma no coverage # noqa: BLE001 + print(f"{location}: {e}") + leap_second_data = None + + if leap_second_data is not None: + print(f"{location}: Last updated {leap_second_data.last_updated}") + print(f"{location}: Valid until {leap_second_data.valid_until}") + else: + print(f"{location}: Could not be read") + + if __name__ == "__main__": # pragma no cover cli() diff --git a/pyproject.toml b/pyproject.toml index 86ef464..cccc58a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ write_to = "leapseconddata/__version__.py" line-length=120 [tool.ruff.lint] select = ["E", "F", "D", "I", "N", "UP", "YTT", "BLE", "B", "FBT", "A", "COM", "C4", "DTZ", "FA", "ISC", "ICN", "PIE", "PYI", "Q", "RET", "SIM", "TID", "TCH", "ARG", "PTH", "C", "R", "W", "FLY", "RUF", "PL"] -ignore = ["D203", "D213", "D400", "D415", "ISC001"] +ignore = ["D203", "D213", "D400", "D415", "ISC001", "COM812"] [project] name = "leapseconddata" authors = [{name = "Jeff Epler", email = "jepler@gmail.com"}] diff --git a/testleapseconddata.py b/testleapseconddata.py index 3be36c1..4556ff6 100644 --- a/testleapseconddata.py +++ b/testleapseconddata.py @@ -41,6 +41,7 @@ def test_main(self) -> None: self.run_main("next-leapsecond", "2100-2-2") self.run_main("previous-leapsecond", "2009-2-2") self.run_main("previous-leapsecond", "1960-2-2") + self.run_main("sources") def test_corrupt(self) -> None: self.assertRaises(