wwvb 5.0.2__tar.gz → 6.0.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.
Files changed (79) hide show
  1. {wwvb-5.0.2 → wwvb-6.0.0}/.github/workflows/test.yml +31 -5
  2. {wwvb-5.0.2 → wwvb-6.0.0}/.pre-commit-config.yaml +1 -1
  3. wwvb-6.0.0/.readthedocs.yaml +17 -0
  4. {wwvb-5.0.2 → wwvb-6.0.0}/Makefile +1 -1
  5. {wwvb-5.0.2/src/wwvb.egg-info → wwvb-6.0.0}/PKG-INFO +6 -8
  6. {wwvb-5.0.2 → wwvb-6.0.0}/README.md +5 -7
  7. {wwvb-5.0.2 → wwvb-6.0.0}/doc/conf.py +5 -0
  8. wwvb-6.0.0/doc/index.rst +24 -0
  9. {wwvb-5.0.2 → wwvb-6.0.0}/pyproject.toml +3 -0
  10. {wwvb-5.0.2 → wwvb-6.0.0}/requirements-dev.txt +2 -1
  11. {wwvb-5.0.2 → wwvb-6.0.0}/src/uwwvb.py +5 -2
  12. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/__init__.py +91 -36
  13. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/__version__.py +2 -2
  14. wwvb-6.0.0/src/wwvb/iersdata.json +1 -0
  15. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/wwvbtk.py +16 -18
  16. {wwvb-5.0.2 → wwvb-6.0.0/src/wwvb.egg-info}/PKG-INFO +6 -8
  17. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/SOURCES.txt +1 -3
  18. {wwvb-5.0.2 → wwvb-6.0.0}/test/testuwwvb.py +1 -1
  19. {wwvb-5.0.2 → wwvb-6.0.0}/test/testwwvb.py +32 -1
  20. wwvb-5.0.2/.github/workflows/codeql.yml +0 -49
  21. wwvb-5.0.2/LICENSES/Apache-2.0.txt +0 -73
  22. wwvb-5.0.2/adafruit_datetime.pyi +0 -416
  23. wwvb-5.0.2/doc/index.rst +0 -31
  24. wwvb-5.0.2/src/wwvb/iersdata.json +0 -1
  25. {wwvb-5.0.2 → wwvb-6.0.0}/.coveragerc +0 -0
  26. {wwvb-5.0.2 → wwvb-6.0.0}/.github/workflows/cron.yml +0 -0
  27. {wwvb-5.0.2 → wwvb-6.0.0}/.github/workflows/release.yml +0 -0
  28. {wwvb-5.0.2 → wwvb-6.0.0}/.gitignore +0 -0
  29. {wwvb-5.0.2 → wwvb-6.0.0}/LICENSES/CC0-1.0.txt +0 -0
  30. {wwvb-5.0.2 → wwvb-6.0.0}/LICENSES/GPL-3.0-only.txt +0 -0
  31. {wwvb-5.0.2 → wwvb-6.0.0}/LICENSES/Unlicense.txt +0 -0
  32. {wwvb-5.0.2 → wwvb-6.0.0}/codecov.yml +0 -0
  33. {wwvb-5.0.2 → wwvb-6.0.0}/doc/_static/.empty +0 -0
  34. {wwvb-5.0.2 → wwvb-6.0.0}/requirements.txt +0 -0
  35. {wwvb-5.0.2 → wwvb-6.0.0}/setup.cfg +0 -0
  36. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/decode.py +0 -0
  37. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/dut1table.py +0 -0
  38. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/gen.py +0 -0
  39. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/iersdata.json.license +0 -0
  40. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/iersdata.py +0 -0
  41. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/py.typed +0 -0
  42. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/tz.py +0 -0
  43. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/updateiers.py +0 -0
  44. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/dependency_links.txt +0 -0
  45. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/entry_points.txt +0 -0
  46. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/requires.txt +0 -0
  47. {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/top_level.txt +0 -0
  48. {wwvb-5.0.2 → wwvb-6.0.0}/test/testcli.py +0 -0
  49. {wwvb-5.0.2 → wwvb-6.0.0}/test/testdaylight.py +0 -0
  50. {wwvb-5.0.2 → wwvb-6.0.0}/test/testls.py +0 -0
  51. {wwvb-5.0.2 → wwvb-6.0.0}/test/testpm.py +0 -0
  52. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/1998leapsecond +0 -0
  53. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/2012leapsecond +0 -0
  54. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/all-headers +0 -0
  55. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/bar +0 -0
  56. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/both +0 -0
  57. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/cradek +0 -0
  58. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/duration +0 -0
  59. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/enddst-phase +0 -0
  60. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/enddst-phase-2 +0 -0
  61. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/endleapyear +0 -0
  62. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/leapday1 +0 -0
  63. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/leapday28 +0 -0
  64. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/leapday29 +0 -0
  65. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/negleapsecond +0 -0
  66. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nextdst +0 -0
  67. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nextst +0 -0
  68. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nonleapday1 +0 -0
  69. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nonleapday28 +0 -0
  70. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/phase +0 -0
  71. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startdst +0 -0
  72. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startdst-phase +0 -0
  73. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startdst-phase-2 +0 -0
  74. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startleapyear +0 -0
  75. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startst +0 -0
  76. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k +0 -0
  77. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k-1 +0 -0
  78. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k1 +0 -0
  79. {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k1-1 +0 -0
@@ -31,6 +31,35 @@ jobs:
31
31
  - name: Build HTML docs
32
32
  run: make html
33
33
 
34
+ typing:
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ python-version:
39
+ - '3.13'
40
+ os-version:
41
+ - 'ubuntu-latest'
42
+ runs-on: ${{ matrix.os-version }}
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ with:
46
+ persist-credentials: false
47
+
48
+ - name: Set up Python
49
+ uses: actions/setup-python@v5
50
+ with:
51
+ python-version: ${{ matrix.python-version }}
52
+
53
+ - name: Install deps
54
+ run: |
55
+ python -mpip install wheel
56
+ python -mpip install -r requirements-dev.txt
57
+
58
+ - name: Check stubs
59
+ if: (! startsWith(matrix.python-version, 'pypy-'))
60
+ run: make mypy PYTHON=python
61
+
62
+
34
63
  test:
35
64
  strategy:
36
65
  fail-fast: false
@@ -40,7 +69,8 @@ jobs:
40
69
  - '3.10'
41
70
  - '3.11'
42
71
  - '3.12'
43
- - '3.13.0-alpha.0 - 3.13'
72
+ - '3.13'
73
+ - '3.14.0-alpha.0 - 3.14'
44
74
  os-version:
45
75
  - 'ubuntu-latest'
46
76
  include:
@@ -67,10 +97,6 @@ jobs:
67
97
  python -mpip install wheel
68
98
  python -mpip install -r requirements-dev.txt
69
99
 
70
- - name: Check stubs
71
- if: (! startsWith(matrix.python-version, 'pypy-'))
72
- run: make mypy PYTHON=python
73
-
74
100
  - name: Coverage
75
101
  run: make coverage PYTHON=python
76
102
 
@@ -21,7 +21,7 @@ repos:
21
21
  - id: reuse
22
22
  - repo: https://github.com/astral-sh/ruff-pre-commit
23
23
  # Ruff version.
24
- rev: v0.11.5
24
+ rev: v0.12.1
25
25
  hooks:
26
26
  # Run the linter.
27
27
  - id: ruff
@@ -0,0 +1,17 @@
1
+ # SPDX-FileCopyrightText: 2024 Jeff Epler
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+
5
+ version: 2
6
+
7
+ build:
8
+ os: ubuntu-lts-latest
9
+ tools:
10
+ python: "3"
11
+
12
+ sphinx:
13
+ configuration: doc/conf.py
14
+
15
+ python:
16
+ install:
17
+ - requirements: requirements-dev.txt
@@ -44,7 +44,7 @@ test_venv:
44
44
 
45
45
  .PHONY: mypy
46
46
  mypy:
47
- $(Q)mypy --strict --no-warn-unused-ignores src
47
+ $(Q)mypy --strict --no-warn-unused-ignores src test
48
48
 
49
49
  .PHONY: update
50
50
  update:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wwvb
3
- Version: 5.0.2
3
+ Version: 6.0.0
4
4
  Summary: Generate WWVB timecodes for any desired time
5
5
  Author-email: Jeff Epler <jepler@gmail.com>
6
6
  Project-URL: Source, https://github.com/jepler/wwvbpy
@@ -33,13 +33,11 @@ SPDX-License-Identifier: GPL-3.0-only
33
33
  [![codecov](https://codecov.io/gh/jepler/wwvbpy/branch/main/graph/badge.svg?token=Exx0c3Gp65)](https://codecov.io/gh/jepler/wwvbpy)
34
34
  [![Update DUT1 data](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
35
35
  [![PyPI](https://img.shields.io/pypi/v/wwvb)](https://pypi.org/project/wwvb)
36
- [![CodeQL](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml)
37
36
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jepler/wwvbpy/main.svg)](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
38
37
 
39
38
  # Purpose
40
39
 
41
- wwvbpy generates WWVB timecodes for any desired time. These timecodes
42
- may be useful in testing WWVB decoder software.
40
+ Python package and command line programs for interacting with WWVB timecodes.
43
41
 
44
42
  Where possible, wwvbpy uses existing facilities for calendar and time
45
43
  manipulation (datetime and dateutil).
@@ -65,7 +63,7 @@ The package includes:
65
63
 
66
64
  # Development status
67
65
 
68
- The author (@jepler) occasionally develops and maintains this project, but
66
+ The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
69
67
  issues are not likely to be acted on. They would be interested in adding
70
68
  co-maintainer(s).
71
69
 
@@ -93,7 +91,7 @@ channel.
93
91
  # Usage
94
92
 
95
93
  ~~~~
96
- Usage: python -m wwvb.gen [OPTIONS] [TIMESPEC]...
94
+ Usage: wwvbgen [OPTIONS] [TIMESPEC]...
97
95
 
98
96
  Generate WWVB timecodes
99
97
 
@@ -123,7 +121,7 @@ Options:
123
121
 
124
122
  For example, to display the leap second that occurred at the end of 1998,
125
123
  ~~~~
126
- $ python wwvbgen.py -m 7 1998 365 23 56
124
+ $ wwvbgen -m 7 1998 365 23 56
127
125
  WWVB timecode: year=98 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1
128
126
  '98+365 23:56 210100110200100001120011001102010100010200110100121000001002
129
127
  '98+365 23:57 210100111200100001120011001102010100010200110100121000001002
@@ -145,7 +143,7 @@ The letters `a` through `u` represent offsets of -1.0s through +1.0s
145
143
  in 0.1s increments; `k` represents 0s. (In practice, only a smaller range
146
144
  of values, typically -0.7s to +0.8s, is seen)
147
145
 
148
- For 2001 through 2019, NIST has published the actual DUT1 values broadcast,
146
+ For 2001 through 2024, NIST has published the actual DUT1 values broadcast,
149
147
  and the date of each change, though it in the format of an HTML
150
148
  table and not designed for machine readability:
151
149
 
@@ -7,13 +7,11 @@ SPDX-License-Identifier: GPL-3.0-only
7
7
  [![codecov](https://codecov.io/gh/jepler/wwvbpy/branch/main/graph/badge.svg?token=Exx0c3Gp65)](https://codecov.io/gh/jepler/wwvbpy)
8
8
  [![Update DUT1 data](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
9
9
  [![PyPI](https://img.shields.io/pypi/v/wwvb)](https://pypi.org/project/wwvb)
10
- [![CodeQL](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml)
11
10
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jepler/wwvbpy/main.svg)](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
12
11
 
13
12
  # Purpose
14
13
 
15
- wwvbpy generates WWVB timecodes for any desired time. These timecodes
16
- may be useful in testing WWVB decoder software.
14
+ Python package and command line programs for interacting with WWVB timecodes.
17
15
 
18
16
  Where possible, wwvbpy uses existing facilities for calendar and time
19
17
  manipulation (datetime and dateutil).
@@ -39,7 +37,7 @@ The package includes:
39
37
 
40
38
  # Development status
41
39
 
42
- The author (@jepler) occasionally develops and maintains this project, but
40
+ The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
43
41
  issues are not likely to be acted on. They would be interested in adding
44
42
  co-maintainer(s).
45
43
 
@@ -67,7 +65,7 @@ channel.
67
65
  # Usage
68
66
 
69
67
  ~~~~
70
- Usage: python -m wwvb.gen [OPTIONS] [TIMESPEC]...
68
+ Usage: wwvbgen [OPTIONS] [TIMESPEC]...
71
69
 
72
70
  Generate WWVB timecodes
73
71
 
@@ -97,7 +95,7 @@ Options:
97
95
 
98
96
  For example, to display the leap second that occurred at the end of 1998,
99
97
  ~~~~
100
- $ python wwvbgen.py -m 7 1998 365 23 56
98
+ $ wwvbgen -m 7 1998 365 23 56
101
99
  WWVB timecode: year=98 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1
102
100
  '98+365 23:56 210100110200100001120011001102010100010200110100121000001002
103
101
  '98+365 23:57 210100111200100001120011001102010100010200110100121000001002
@@ -119,7 +117,7 @@ The letters `a` through `u` represent offsets of -1.0s through +1.0s
119
117
  in 0.1s increments; `k` represents 0s. (In practice, only a smaller range
120
118
  of values, typically -0.7s to +0.8s, is seen)
121
119
 
122
- For 2001 through 2019, NIST has published the actual DUT1 values broadcast,
120
+ For 2001 through 2024, NIST has published the actual DUT1 values broadcast,
123
121
  and the date of each change, though it in the format of an HTML
124
122
  table and not designed for machine readability:
125
123
 
@@ -56,6 +56,7 @@ version = release = final_version
56
56
  # ones.
57
57
  extensions = [
58
58
  "sphinx.ext.autodoc",
59
+ "sphinx_mdinclude",
59
60
  ]
60
61
 
61
62
  # Add any paths that contain templates here, relative to this directory.
@@ -82,6 +83,10 @@ html_static_path = ["_static"]
82
83
  autodoc_typehints = "description"
83
84
  autodoc_class_signature = "separated"
84
85
 
86
+ default_role = "any"
87
+
88
+ intersphinx_mapping = {'py': ('https://docs.python.org/3', None)}
89
+
85
90
  # SPDX-FileCopyrightText: 2021-2024 Jeff Epler
86
91
  #
87
92
  # SPDX-License-Identifier: GPL-3.0-only
@@ -0,0 +1,24 @@
1
+ .. SPDX-FileCopyrightText: 2021-2024 Jeff Epler
2
+ ..
3
+ .. SPDX-License-Identifier: GPL-3.0-only
4
+
5
+ wwvbpy |version|
6
+ ================
7
+
8
+ .. mdinclude:: ../README.md
9
+
10
+ .. toctree::
11
+ :maxdepth: 2
12
+ :caption: Contents:
13
+
14
+ wwvb module
15
+ ===========
16
+
17
+ .. automodule:: wwvb
18
+ :members:
19
+
20
+ uwwvb module
21
+ ============
22
+
23
+ .. automodule:: uwwvb
24
+ :members:
@@ -48,3 +48,6 @@ dut1table = "wwvb.dut1table:main"
48
48
  updateiers = "wwvb.updateiers:main"
49
49
  [project.gui-scripts]
50
50
  wwvbtk = "wwvb.wwvbtk:main"
51
+ [[tool.mypy.overrides]]
52
+ module = ["adafruit_datetime"]
53
+ follow_untyped_imports = true
@@ -14,9 +14,10 @@ pre-commit
14
14
  python-dateutil
15
15
  requests; implementation_name=="cpython"
16
16
  setuptools>=68; implementation_name=="cpython"
17
- sphinx>=7,<8
17
+ sphinx
18
18
  sphinx-autodoc-typehints
19
19
  sphinx-rtd-theme
20
+ sphinx-mdinclude
20
21
  twine; implementation_name=="cpython"
21
22
  types-beautifulsoup4; implementation_name=="cpython"
22
23
  types-python-dateutil; implementation_name=="cpython"
@@ -2,9 +2,12 @@
2
2
  #
3
3
  # SPDX-License-Identifier: GPL-3.0-only
4
4
 
5
- # ruff: noqa: C405 PYI024 PLR2004 FBT001 FBT002
5
+ # ruff: noqa: C405, PYI024, FBT001, FBT002
6
6
 
7
- """Implementation of a WWVB state machine & decoder for resource-constrained systems"""
7
+ """Implementation of a WWVB state machine & decoder for resource-constrained systems
8
+
9
+ This version is intended for use with MicroPython & CircuitPython.
10
+ """
8
11
 
9
12
  from __future__ import annotations
10
13
 
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/python3
2
- """A library for WWVB timecodes"""
2
+ """A package and CLI for WWVB timecodes
3
+
4
+ This is the full featured library suitable for use on 'real computers'.
5
+ For a reduced version suitable for use on MicroPython & CircuitPython,
6
+ see `uwwvb`.
7
+
8
+ This package also includes the commandline programs listed above,
9
+ perhaps most importantly ``wwvbgen`` for generating WWVB timecodes.
10
+ """
3
11
 
4
12
  # SPDX-FileCopyrightText: 2011-2024 Jeff Epler
5
13
  #
@@ -24,12 +32,6 @@ SECOND = datetime.timedelta(seconds=1)
24
32
  T = TypeVar("T")
25
33
 
26
34
 
27
- def _require(x: T | None) -> T:
28
- """Check an Optional item is not None."""
29
- assert x is not None
30
- return x
31
-
32
-
33
35
  def _removeprefix(s: str, p: str) -> str:
34
36
  if s.startswith(p):
35
37
  return s[len(p) :]
@@ -323,11 +325,22 @@ _dst_ls_lut = [
323
325
  ]
324
326
 
325
327
 
326
- class _WWVBMinute(NamedTuple):
327
- """Uniquely identifies a minute of time in the WWVB system.
328
+ @enum.unique
329
+ class DstStatus(enum.IntEnum):
330
+ """Constants that describe the DST status of a minute"""
328
331
 
329
- To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
330
- """
332
+ DST_NOT_IN_EFFECT = 0b00
333
+ """DST not in effect today"""
334
+ DST_STARTS_TODAY = 0b01
335
+ """DST starts today at 0200 local standard time"""
336
+ DST_ENDS_TODAY = 0b10
337
+ """DST ends today at 0200 local standard time"""
338
+ DST_IN_EFFECT = 0b11
339
+ """DST in effect all day today"""
340
+
341
+
342
+ class _WWVBMinute(NamedTuple):
343
+ """(implementation detail)"""
331
344
 
332
345
  year: int
333
346
  """2-digit year within the WWVB epoch"""
@@ -341,7 +354,7 @@ class _WWVBMinute(NamedTuple):
341
354
  min: int
342
355
  """Minute of hour"""
343
356
 
344
- dst: int
357
+ dst: DstStatus
345
358
  """2-bit DST code """
346
359
 
347
360
  ut1: int
@@ -357,7 +370,8 @@ class _WWVBMinute(NamedTuple):
357
370
  class WWVBMinute(_WWVBMinute):
358
371
  """Uniquely identifies a minute of time in the WWVB system.
359
372
 
360
- To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
373
+ To use ``ut1`` and ``ls`` information from IERS, create a `WWVBMinuteIERS`
374
+ object instead.
361
375
  """
362
376
 
363
377
  epoch: int = 1970
@@ -368,16 +382,26 @@ class WWVBMinute(_WWVBMinute):
368
382
  days: int,
369
383
  hour: int,
370
384
  minute: int,
371
- dst: int | None = None,
385
+ dst: DstStatus | int | None = None,
372
386
  ut1: int | None = None,
387
+ *,
373
388
  ls: bool | None = None,
374
389
  ly: bool | None = None,
375
390
  ) -> WWVBMinute:
376
- """Construct a WWVBMinute"""
377
- if dst is None:
378
- dst = cls.get_dst(year, days)
379
- if dst not in (0, 1, 2, 3):
380
- raise ValueError("dst value should be 0..3")
391
+ """Construct a WWVBMinute
392
+
393
+ :param year: The 2- or 4-digit year. This parameter is converted by the `full_year` method.
394
+ :param days: 1-based day of year
395
+
396
+ :param hour: UTC hour of day
397
+
398
+ :param minute: Minute of hour
399
+ :param dst: 2-bit DST code
400
+ :param ut1: UT1 offset in units of 100ms, range -900 to +900ms
401
+ :param ls: Leap second warning flag
402
+ :param ly: Leap year flag
403
+ """
404
+ dst = cls.get_dst(year, days) if dst is None else DstStatus(dst)
381
405
  if ut1 is None and ls is None:
382
406
  ut1, ls = cls._get_dut1_info(year, days)
383
407
  elif ut1 is None or ls is None:
@@ -407,13 +431,13 @@ class WWVBMinute(_WWVBMinute):
407
431
  return year
408
432
 
409
433
  @staticmethod
410
- def get_dst(year: int, days: int) -> int:
434
+ def get_dst(year: int, days: int) -> DstStatus:
411
435
  """Get the 2-bit WWVB DST value for the given day"""
412
436
  d0 = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
413
437
  d1 = d0 + datetime.timedelta(1)
414
438
  dst0 = isdst(d0)
415
439
  dst1 = isdst(d1)
416
- return dst1 * 2 + dst0
440
+ return DstStatus(dst1 * 2 + dst0)
417
441
 
418
442
  def __str__(self) -> str:
419
443
  """Implement str()"""
@@ -424,7 +448,10 @@ class WWVBMinute(_WWVBMinute):
424
448
  )
425
449
 
426
450
  def as_datetime_utc(self) -> datetime.datetime:
427
- """Convert to a UTC datetime"""
451
+ """Convert to a UTC datetime
452
+
453
+ The returned object has ``tzinfo=datetime.timezone.utc``.
454
+ """
428
455
  d = datetime.datetime(self.year, 1, 1, tzinfo=datetime.timezone.utc)
429
456
  d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.min * 60)
430
457
  return d
@@ -437,7 +464,18 @@ class WWVBMinute(_WWVBMinute):
437
464
  *,
438
465
  dst_observed: bool = True,
439
466
  ) -> datetime.datetime:
440
- """Convert to a local datetime according to the DST bits"""
467
+ """Convert to a local datetime according to the DST bits
468
+
469
+ The returned object has ``tz=datetime.timezone(computed_offset)``.
470
+
471
+ :param standard_time_offset: The UTC offset of local standard time, in seconds west of UTC.
472
+ The default value, ``7 * 3600``, is for Colorado, the source of the WWVB broadcast.
473
+
474
+ :param dst_observed: If ``True`` then the locale observes DST, and a
475
+ one hour offset is applied according to WWVB rules. If ``False``, then
476
+ the standard time offset is used at all times.
477
+
478
+ """
441
479
  u = self.as_datetime_utc()
442
480
  offset = datetime.timedelta(seconds=-standard_time_offset)
443
481
  d = u - datetime.timedelta(seconds=standard_time_offset)
@@ -622,15 +660,15 @@ class WWVBMinute(_WWVBMinute):
622
660
  else:
623
661
  self._fill_pm_timecode_regular(t)
624
662
 
625
- def next_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
663
+ def next_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
626
664
  """Return an object representing the next minute"""
627
665
  d = self.as_datetime() + datetime.timedelta(minutes=1)
628
- return self.from_datetime(d, newut1, newls, self)
666
+ return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
629
667
 
630
- def previous_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
668
+ def previous_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
631
669
  """Return an object representing the previous minute"""
632
670
  d = self.as_datetime() - datetime.timedelta(minutes=1)
633
- return self.from_datetime(d, newut1, newls, self)
671
+ return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
634
672
 
635
673
  @classmethod
636
674
  def _get_dut1_info(cls: type, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
@@ -659,18 +697,19 @@ class WWVBMinute(_WWVBMinute):
659
697
  days = d.pop("days")
660
698
  hour = d.pop("hour")
661
699
  minute = d.pop("minute")
662
- dst: int | None = d.pop("dst", None)
663
- ut1: int | None = d.pop("ut1", None)
700
+ dst = d.pop("dst", None)
701
+ ut1 = d.pop("ut1", None)
664
702
  ls = d.pop("ls", None)
665
703
  d.pop("ly", None)
666
704
  if d:
667
705
  raise ValueError(f"Invalid options: {d}")
668
- return cls(year, days, hour, minute, dst, ut1, None if ls is None else bool(ls))
706
+ return cls(year, days, hour, minute, dst, ut1=ut1, ls=None if ls is None else bool(ls))
669
707
 
670
708
  @classmethod
671
709
  def from_datetime(
672
710
  cls,
673
711
  d: datetime.datetime,
712
+ *,
674
713
  newut1: int | None = None,
675
714
  newls: bool | None = None,
676
715
  old_time: WWVBMinute | None = None,
@@ -682,7 +721,7 @@ class WWVBMinute(_WWVBMinute):
682
721
  return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
683
722
 
684
723
  @classmethod
685
- def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None:
724
+ def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None: # noqa: PLR0912
686
725
  """Construct a WWVBMinute from a WWVBTimecode"""
687
726
  for i in (0, 9, 19, 29, 39, 49, 59):
688
727
  if t.am[i] != AmplitudeModulation.MARK:
@@ -697,9 +736,13 @@ class WWVBMinute(_WWVBMinute):
697
736
  minute = t._get_am_bcd(1, 2, 3, 5, 6, 7, 8)
698
737
  if minute is None:
699
738
  return None
739
+ if minute >= 60:
740
+ return None
700
741
  hour = t._get_am_bcd(12, 13, 15, 16, 17, 18)
701
742
  if hour is None:
702
743
  return None
744
+ if hour >= 24:
745
+ return None
703
746
  days = t._get_am_bcd(22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
704
747
  if days is None:
705
748
  return None
@@ -716,8 +759,10 @@ class WWVBMinute(_WWVBMinute):
716
759
  if days > 366 or (not ly and days > 365):
717
760
  return None
718
761
  ls = bool(t.am[56])
719
- dst = _require(t._get_am_bcd(57, 58))
720
- return cls(year, days, hour, minute, dst, ut1, ls, ly)
762
+ dst = t._get_am_bcd(57, 58)
763
+ if dst is None:
764
+ return None
765
+ return cls(year, days, hour, minute, dst, ut1, ls=ls, ly=ly)
721
766
 
722
767
 
723
768
  class WWVBMinuteIERS(WWVBMinute):
@@ -729,7 +774,7 @@ class WWVBMinuteIERS(WWVBMinute):
729
774
  return round(get_dut1(d) * 10) * 100, isls(d)
730
775
 
731
776
 
732
- def _bcd_bits(n: int) -> Generator[bool, None, None]:
777
+ def _bcd_bits(n: int) -> Generator[bool]:
733
778
  """Return the bcd representation of n, starting with the least significant bit"""
734
779
  while True:
735
780
  d = n % 10
@@ -743,9 +788,13 @@ class AmplitudeModulation(enum.IntEnum):
743
788
  """Constants that describe an Amplitude Modulation value"""
744
789
 
745
790
  ZERO = 0
791
+ """A zero bit (reduced carrier during the first 200ms of the second)"""
746
792
  ONE = 1
793
+ """A one bit (reduced carrier during the first 500ms of the second)"""
747
794
  MARK = 2
795
+ """A mark bit (reduced carrier during the first 800ms of the second)"""
748
796
  UNSET = -1
797
+ """An unset or unknown amplitude modulation value"""
749
798
 
750
799
 
751
800
  @enum.unique
@@ -753,8 +802,11 @@ class PhaseModulation(enum.IntEnum):
753
802
  """Constants that describe a Phase Modulation value"""
754
803
 
755
804
  ZERO = 0
805
+ """A one bit (180° phase shift during the second)"""
756
806
  ONE = 1
807
+ """A zero bit (No phase shift during the second)"""
757
808
  UNSET = -1
809
+ """An unset or unknown phase modulation value"""
758
810
 
759
811
 
760
812
  class WWVBTimecode:
@@ -777,7 +829,10 @@ class WWVBTimecode:
777
829
  The the bits ``self.am[poslist[i]]`` in MSB order are converted from
778
830
  BCD to integer
779
831
  """
780
- pos = reversed(poslist)
832
+ pos = list(poslist)[::-1]
833
+ for p in pos:
834
+ if self.am[p] not in {AmplitudeModulation.ZERO, AmplitudeModulation.ONE}:
835
+ return None
781
836
  val = [bool(self.am[p]) for p in pos]
782
837
  result = 0
783
838
  base = 1
@@ -805,7 +860,7 @@ class WWVBTimecode:
805
860
  else:
806
861
  self.am[p] = AmplitudeModulation.ZERO
807
862
 
808
- def _put_pm_bit(self, i: int, v: PhaseModulation | int | bool) -> None:
863
+ def _put_pm_bit(self, i: int, v: PhaseModulation | int) -> None:
809
864
  """Update a bit of the Phase Modulation signal"""
810
865
  self.phase[i] = PhaseModulation(v)
811
866
 
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '5.0.2'
21
- __version_tuple__ = version_tuple = (5, 0, 2)
20
+ __version__ = version = '6.0.0'
21
+ __version_tuple__ = version_tuple = (6, 0, 0)
@@ -0,0 +1 @@
1
+ {"START": "1972-01-01", "OFFSETS_GZ": "H4sIAJF3PWgC/+2aa3LDMAiEL5uHLDuxnN5/pn/aTmfSSiAWhGR9J8gsywJylqVHPtqxZuH/7leeI0fKsGd5EngQ2WisJWKegrThDa6aJFnL0u4wYZkCE2UmSF0U+13vCveStC6JTfQyW3O86HLJf0SvDgy5u4FCI+WVKRuy0KMjJeXoULIvMDmEWgeRxAJtwXquPCIBqbLh/gbfv0mcxk3mHV9tYiATZP8W/zgw2wd5LpJnY+WErI8abJ3opaIW6592+YMbjSsNWQFlNVVtuhjhtQzSUh4MEpOdDrSW6qsUv+O+Dt+XkIONSrUwvWmTsmq5LO9xsZ+EgcDK+MIESDaYmxSxGlgbGOFjBXMjbV7lc6zlmQ0i48oH5P4+vK7i/AHc7tfTXDtffqFi3m6WhApPSTyDvArU5vUDhm7YaNQYGASVbbwLUBtI2PrhSiZNbvCRrtGUGu0GbjDhJ3aLCx5dQFjt0LFovmWB96e6tktqMenoULXajVS3asBibP3kYXrpmZxnsS2Yf2xRPrHbvQ2D9wjfL4C6b4PWV4otW0vWUYkeWE5M8M594oLbxP77xcl4NuBkG0dfM3xOUf/T0GF+ur+J5pljcODEUZkXg6vIdLYy7g3oZU3bPNDnc8qwGdJZMmAurUsRj6tOo95zP6fb9YPWp5OuZ5X7q2DrmsG/VCyTyaREnDRhnUxOzfzzh3/NRuYTMxwhU6lNAAA="}
@@ -6,8 +6,8 @@
6
6
  # SPDX-License-Identifier: GPL-3.0-only
7
7
  from __future__ import annotations
8
8
 
9
+ import datetime
9
10
  import functools
10
- import time
11
11
  from tkinter import Canvas, TclError, Tk
12
12
  from typing import TYPE_CHECKING, Any
13
13
 
@@ -59,31 +59,29 @@ DEFAULT_COLORS = "#3c3c3c #3c3c3c #3c3c3c #cc3c3c #88883c #3ccc3c"
59
59
  metavar="COLORS",
60
60
  help="2, 3, 4, or 6 Tk color values",
61
61
  )
62
- @click.option("--size", default=48)
63
- @click.option("--min-size", default=None)
62
+ @click.option("--size", default=48, help="initial size in pixels")
63
+ @click.option("--min-size", default=None, type=int, help="minimum size in pixels (default: same as initial size)")
64
64
  def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: PLR0915
65
65
  """Visualize the WWVB signal in realtime"""
66
66
  if min_size is None:
67
67
  min_size = size
68
68
 
69
- def deadline_ms(deadline: float) -> int:
69
+ def deadline_ms(deadline: datetime.datetime) -> int:
70
70
  """Compute the number of ms until a deadline"""
71
- now = time.time()
72
- return int(max(0, deadline - now) * 1000)
71
+ now = datetime.datetime.now(datetime.timezone.utc)
72
+ return int(max(0, (deadline - now).total_seconds()) * 1000)
73
73
 
74
- def wwvbtick() -> Generator[tuple[float, wwvb.AmplitudeModulation], None, None]:
74
+ def wwvbtick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
75
75
  """Yield consecutive values of the WWVB amplitude signal, going from minute to minute"""
76
- timestamp = time.time() // 60 * 60
76
+ timestamp = datetime.datetime.now(datetime.timezone.utc).replace(second=0, microsecond=0)
77
77
 
78
78
  while True:
79
- tt = time.gmtime(timestamp)
80
- key = tt.tm_year, tt.tm_yday, tt.tm_hour, tt.tm_min
81
- timecode = wwvb.WWVBMinuteIERS(*key).as_timecode()
79
+ timecode = wwvb.WWVBMinuteIERS.from_datetime(timestamp).as_timecode()
82
80
  for i, code in enumerate(timecode.am):
83
- yield timestamp + i, code
84
- timestamp = timestamp + 60
81
+ yield timestamp + datetime.timedelta(seconds=i), code
82
+ timestamp = timestamp + datetime.timedelta(seconds=60)
85
83
 
86
- def wwvbsmarttick() -> Generator[tuple[float, wwvb.AmplitudeModulation], None, None]:
84
+ def wwvbsmarttick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
87
85
  """Yield consecutive values of the WWVB amplitude signal
88
86
 
89
87
  .. but deal with time progressing unexpectedly, such as when the
@@ -94,10 +92,10 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
94
92
  """
95
93
  while True:
96
94
  for stamp, code in wwvbtick():
97
- now = time.time()
98
- if stamp < now - 60:
95
+ now = datetime.datetime.now(datetime.timezone.utc)
96
+ if stamp < now - datetime.timedelta(seconds=60):
99
97
  break
100
- if stamp < now - 1:
98
+ if stamp < now - datetime.timedelta(seconds=1):
101
99
  continue
102
100
  yield stamp, code
103
101
 
@@ -137,7 +135,7 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
137
135
  yield deadline_ms(stamp)
138
136
  led_on(code)
139
137
  app.update()
140
- yield deadline_ms(stamp + 0.2 + 0.3 * int(code))
138
+ yield deadline_ms(stamp + datetime.timedelta(seconds=0.2 + 0.3 * int(code)))
141
139
  led_off(code)
142
140
  app.update()
143
141