From 887f6535f2fcda5cbbf6a87cac5e3137a0aa2685 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 17 Jul 2024 21:13:36 -0500 Subject: [PATCH 1/7] Fix doc build Items in the `_env` directory must not be considered --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ------------------------------------------------- From 58b6d4f0aa01894433519814912f6a678011c75f Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 17 Jul 2024 21:18:21 -0500 Subject: [PATCH 2/7] Document data sources for leap second data --- leapseconddata/__init__.py | 54 ++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/leapseconddata/__init__.py b/leapseconddata/__init__.py index 60c0d18..00e8288 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,7 +282,7 @@ 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: From 30ee684c7d598c4f5dc1b26b6b9ac0d7189a85be Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 17 Jul 2024 21:18:28 -0500 Subject: [PATCH 3/7] Disable conflicting ruff rule --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"}] From ddd9a0e6ab941755e68d347d1ef5e3ebcbf98df2 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 17 Jul 2024 21:18:37 -0500 Subject: [PATCH 4/7] Add command to print info about data sources --- leapseconddata/__main__.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/leapseconddata/__main__.py b/leapseconddata/__main__.py index 44476ea..1a2486f 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 = False + 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: + print(f"{location}: Invalid hash") + leap_second_data = LeapSecondData.from_url(location, check_hash=False) + except Exception as e: # 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() From 2bd84cc2d4be3875b455629f1662a79ae2c2e66e Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 17 Jul 2024 21:40:42 -0500 Subject: [PATCH 5/7] fix coverage & test --- leapseconddata/__main__.py | 6 +++--- testleapseconddata.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/leapseconddata/__main__.py b/leapseconddata/__main__.py index 1a2486f..42674a7 100644 --- a/leapseconddata/__main__.py +++ b/leapseconddata/__main__.py @@ -168,17 +168,17 @@ def table(ctx: click.Context, *, start: datetime.datetime, end: datetime.datetim @cli.command def sources() -> None: """Print information about leap-second.list data sources""" - first = False + 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: + except InvalidHashError: # pragma no coverage print(f"{location}: Invalid hash") leap_second_data = LeapSecondData.from_url(location, check_hash=False) - except Exception as e: # noqa: BLE001 + except Exception as e: # pragma no coverage # noqa: BLE001 print(f"{location}: {e}") leap_second_data = None 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( From d16422c4523e6311652c9648c16c5a701573ca51 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 17 Jul 2024 21:50:19 -0500 Subject: [PATCH 6/7] from_url no longer has a default --- leapseconddata/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/leapseconddata/__init__.py b/leapseconddata/__init__.py index 00e8288..4ebb7a5 100755 --- a/leapseconddata/__init__.py +++ b/leapseconddata/__init__.py @@ -288,8 +288,7 @@ def from_url( ) -> 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: From b29f68db7211ad58a97bd8e12775cdb9800a6c49 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 17 Jul 2024 22:00:05 -0500 Subject: [PATCH 7/7] attempt to diagnose why a message about an unclosed socket is printed --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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