wwvb 5.0.1__tar.gz → 5.0.3__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.1 → wwvb-5.0.3}/.github/workflows/cron.yml +1 -1
  2. {wwvb-5.0.1 → wwvb-5.0.3}/.github/workflows/release.yml +1 -1
  3. {wwvb-5.0.1 → wwvb-5.0.3}/.github/workflows/test.yml +27 -5
  4. {wwvb-5.0.1 → wwvb-5.0.3}/.pre-commit-config.yaml +1 -1
  5. wwvb-5.0.3/.readthedocs.yaml +17 -0
  6. {wwvb-5.0.1/src/wwvb.egg-info → wwvb-5.0.3}/PKG-INFO +7 -9
  7. {wwvb-5.0.1 → wwvb-5.0.3}/README.md +5 -7
  8. {wwvb-5.0.1 → wwvb-5.0.3}/doc/conf.py +5 -0
  9. wwvb-5.0.3/doc/index.rst +24 -0
  10. {wwvb-5.0.1 → wwvb-5.0.3}/pyproject.toml +1 -1
  11. {wwvb-5.0.1 → wwvb-5.0.3}/requirements-dev.txt +2 -1
  12. {wwvb-5.0.1 → wwvb-5.0.3}/src/uwwvb.py +4 -1
  13. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/__init__.py +81 -29
  14. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/__version__.py +9 -4
  15. wwvb-5.0.3/src/wwvb/iersdata.json +1 -0
  16. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/updateiers.py +2 -2
  17. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/wwvbtk.py +35 -28
  18. {wwvb-5.0.1 → wwvb-5.0.3/src/wwvb.egg-info}/PKG-INFO +7 -9
  19. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb.egg-info/SOURCES.txt +1 -1
  20. {wwvb-5.0.1 → wwvb-5.0.3}/test/testwwvb.py +32 -0
  21. wwvb-5.0.1/.github/workflows/codeql.yml +0 -49
  22. wwvb-5.0.1/doc/index.rst +0 -31
  23. wwvb-5.0.1/src/wwvb/iersdata.json +0 -1
  24. {wwvb-5.0.1 → wwvb-5.0.3}/.coveragerc +0 -0
  25. {wwvb-5.0.1 → wwvb-5.0.3}/.gitignore +0 -0
  26. {wwvb-5.0.1 → wwvb-5.0.3}/LICENSES/Apache-2.0.txt +0 -0
  27. {wwvb-5.0.1 → wwvb-5.0.3}/LICENSES/CC0-1.0.txt +0 -0
  28. {wwvb-5.0.1 → wwvb-5.0.3}/LICENSES/GPL-3.0-only.txt +0 -0
  29. {wwvb-5.0.1 → wwvb-5.0.3}/LICENSES/Unlicense.txt +0 -0
  30. {wwvb-5.0.1 → wwvb-5.0.3}/Makefile +0 -0
  31. {wwvb-5.0.1 → wwvb-5.0.3}/adafruit_datetime.pyi +0 -0
  32. {wwvb-5.0.1 → wwvb-5.0.3}/codecov.yml +0 -0
  33. {wwvb-5.0.1 → wwvb-5.0.3}/doc/_static/.empty +0 -0
  34. {wwvb-5.0.1 → wwvb-5.0.3}/requirements.txt +0 -0
  35. {wwvb-5.0.1 → wwvb-5.0.3}/setup.cfg +0 -0
  36. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/decode.py +0 -0
  37. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/dut1table.py +0 -0
  38. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/gen.py +0 -0
  39. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/iersdata.json.license +0 -0
  40. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/iersdata.py +0 -0
  41. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/py.typed +0 -0
  42. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb/tz.py +0 -0
  43. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb.egg-info/dependency_links.txt +0 -0
  44. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb.egg-info/entry_points.txt +0 -0
  45. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb.egg-info/requires.txt +0 -0
  46. {wwvb-5.0.1 → wwvb-5.0.3}/src/wwvb.egg-info/top_level.txt +0 -0
  47. {wwvb-5.0.1 → wwvb-5.0.3}/test/testcli.py +0 -0
  48. {wwvb-5.0.1 → wwvb-5.0.3}/test/testdaylight.py +0 -0
  49. {wwvb-5.0.1 → wwvb-5.0.3}/test/testls.py +0 -0
  50. {wwvb-5.0.1 → wwvb-5.0.3}/test/testpm.py +0 -0
  51. {wwvb-5.0.1 → wwvb-5.0.3}/test/testuwwvb.py +0 -0
  52. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/1998leapsecond +0 -0
  53. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/2012leapsecond +0 -0
  54. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/all-headers +0 -0
  55. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/bar +0 -0
  56. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/both +0 -0
  57. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/cradek +0 -0
  58. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/duration +0 -0
  59. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/enddst-phase +0 -0
  60. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/enddst-phase-2 +0 -0
  61. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/endleapyear +0 -0
  62. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/leapday1 +0 -0
  63. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/leapday28 +0 -0
  64. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/leapday29 +0 -0
  65. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/negleapsecond +0 -0
  66. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/nextdst +0 -0
  67. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/nextst +0 -0
  68. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/nonleapday1 +0 -0
  69. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/nonleapday28 +0 -0
  70. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/phase +0 -0
  71. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/startdst +0 -0
  72. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/startdst-phase +0 -0
  73. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/startdst-phase-2 +0 -0
  74. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/startleapyear +0 -0
  75. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/startst +0 -0
  76. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/y2k +0 -0
  77. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/y2k-1 +0 -0
  78. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/y2k1 +0 -0
  79. {wwvb-5.0.1 → wwvb-5.0.3}/test/wwvbgen_testcases/y2k1-1 +0 -0
@@ -11,7 +11,7 @@ on:
11
11
 
12
12
  jobs:
13
13
  update-dut1:
14
- runs-on: ubuntu-20.04
14
+ runs-on: ubuntu-24.04
15
15
  if: startswith(github.repository, 'jepler/')
16
16
  steps:
17
17
 
@@ -11,7 +11,7 @@ on:
11
11
  jobs:
12
12
  release:
13
13
 
14
- runs-on: ubuntu-20.04
14
+ runs-on: ubuntu-24.04
15
15
  steps:
16
16
  - name: Dump GitHub context
17
17
  env:
@@ -31,6 +31,31 @@ 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
+
34
59
  test:
35
60
  strategy:
36
61
  fail-fast: false
@@ -40,7 +65,8 @@ jobs:
40
65
  - '3.10'
41
66
  - '3.11'
42
67
  - '3.12'
43
- - '3.13.0-alpha.0 - 3.13'
68
+ - '3.13'
69
+ - '3.14.0-alpha.0 - 3.14'
44
70
  os-version:
45
71
  - 'ubuntu-latest'
46
72
  include:
@@ -67,10 +93,6 @@ jobs:
67
93
  python -mpip install wheel
68
94
  python -mpip install -r requirements-dev.txt
69
95
 
70
- - name: Check stubs
71
- if: (! startsWith(matrix.python-version, 'pypy-'))
72
- run: make mypy PYTHON=python
73
-
74
96
  - name: Coverage
75
97
  run: make coverage PYTHON=python
76
98
 
@@ -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.8.4
24
+ rev: v0.11.7
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
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: wwvb
3
- Version: 5.0.1
3
+ Version: 5.0.3
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:
@@ -19,7 +19,7 @@ dependencies = {file = "requirements.txt"}
19
19
  write_to = "src/wwvb/__version__.py"
20
20
  [tool.ruff.lint]
21
21
  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"]
22
- ignore = ["D203", "D213", "D400", "D415", "ISC001", "E741", "C901", "PLR0911", "PLR2004", "PLR0913"]
22
+ ignore = ["D203", "D213", "D400", "D415", "ISC001", "E741", "C901", "PLR0911", "PLR2004", "PLR0913", "COM812"]
23
23
  [tool.ruff]
24
24
  line-length = 120
25
25
  [project]
@@ -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"
@@ -4,7 +4,10 @@
4
4
 
5
5
  # ruff: noqa: C405 PYI024 PLR2004 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) :]
@@ -49,7 +51,6 @@ def _maybe_warn_update(dt: datetime.date, stacklevel: int = 1) -> None:
49
51
  # If the date is less than 300 days after today, there should be (possibly)
50
52
  # prospective available now.
51
53
  today = datetime.datetime.now(tz=datetime.timezone.utc).date()
52
- print(f"_mwu {today=!r} {dt=!r} {iersdata.end=!r}")
53
54
  if _date(dt) < today + datetime.timedelta(days=330):
54
55
  warnings.warn(
55
56
  "Note: Running `updateiers` may provide better DUT1 and LS information",
@@ -324,11 +325,22 @@ _dst_ls_lut = [
324
325
  ]
325
326
 
326
327
 
327
- class _WWVBMinute(NamedTuple):
328
- """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"""
329
331
 
330
- To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
331
- """
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)"""
332
344
 
333
345
  year: int
334
346
  """2-digit year within the WWVB epoch"""
@@ -342,7 +354,7 @@ class _WWVBMinute(NamedTuple):
342
354
  min: int
343
355
  """Minute of hour"""
344
356
 
345
- dst: int
357
+ dst: DstStatus
346
358
  """2-bit DST code """
347
359
 
348
360
  ut1: int
@@ -358,7 +370,8 @@ class _WWVBMinute(NamedTuple):
358
370
  class WWVBMinute(_WWVBMinute):
359
371
  """Uniquely identifies a minute of time in the WWVB system.
360
372
 
361
- 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.
362
375
  """
363
376
 
364
377
  epoch: int = 1970
@@ -369,16 +382,25 @@ class WWVBMinute(_WWVBMinute):
369
382
  days: int,
370
383
  hour: int,
371
384
  minute: int,
372
- dst: int | None = None,
385
+ dst: DstStatus | int | None = None,
373
386
  ut1: int | None = None,
374
387
  ls: bool | None = None,
375
388
  ly: bool | None = None,
376
389
  ) -> WWVBMinute:
377
- """Construct a WWVBMinute"""
378
- if dst is None:
379
- dst = cls.get_dst(year, days)
380
- if dst not in (0, 1, 2, 3):
381
- raise ValueError("dst value should be 0..3")
390
+ """Construct a WWVBMinute
391
+
392
+ :param year: The 2- or 4-digit year. This parameter is converted by the `full_year` method.
393
+ :param days: 1-based day of year
394
+
395
+ :param hour: UTC hour of day
396
+
397
+ :param minute: Minute of hour
398
+ :param dst: 2-bit DST code
399
+ :param ut1: UT1 offset in units of 100ms, range -900 to +900ms
400
+ :param ls: Leap second warning flag
401
+ :param ly: Leap year flag
402
+ """
403
+ dst = cls.get_dst(year, days) if dst is None else DstStatus(dst)
382
404
  if ut1 is None and ls is None:
383
405
  ut1, ls = cls._get_dut1_info(year, days)
384
406
  elif ut1 is None or ls is None:
@@ -408,13 +430,13 @@ class WWVBMinute(_WWVBMinute):
408
430
  return year
409
431
 
410
432
  @staticmethod
411
- def get_dst(year: int, days: int) -> int:
433
+ def get_dst(year: int, days: int) -> DstStatus:
412
434
  """Get the 2-bit WWVB DST value for the given day"""
413
435
  d0 = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
414
436
  d1 = d0 + datetime.timedelta(1)
415
437
  dst0 = isdst(d0)
416
438
  dst1 = isdst(d1)
417
- return dst1 * 2 + dst0
439
+ return DstStatus(dst1 * 2 + dst0)
418
440
 
419
441
  def __str__(self) -> str:
420
442
  """Implement str()"""
@@ -425,7 +447,10 @@ class WWVBMinute(_WWVBMinute):
425
447
  )
426
448
 
427
449
  def as_datetime_utc(self) -> datetime.datetime:
428
- """Convert to a UTC datetime"""
450
+ """Convert to a UTC datetime
451
+
452
+ The returned object has ``tzinfo=datetime.timezone.utc``.
453
+ """
429
454
  d = datetime.datetime(self.year, 1, 1, tzinfo=datetime.timezone.utc)
430
455
  d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.min * 60)
431
456
  return d
@@ -438,7 +463,18 @@ class WWVBMinute(_WWVBMinute):
438
463
  *,
439
464
  dst_observed: bool = True,
440
465
  ) -> datetime.datetime:
441
- """Convert to a local datetime according to the DST bits"""
466
+ """Convert to a local datetime according to the DST bits
467
+
468
+ The returned object has ``tz=datetime.timezone(computed_offset)``.
469
+
470
+ :param standard_time_offset: The UTC offset of local standard time, in seconds west of UTC.
471
+ The default value, ``7 * 3600``, is for Colorado, the source of the WWVB broadcast.
472
+
473
+ :param dst_observed: If ``True`` then the locale observes DST, and a
474
+ one hour offset is applied according to WWVB rules. If ``False``, then
475
+ the standard time offset is used at all times.
476
+
477
+ """
442
478
  u = self.as_datetime_utc()
443
479
  offset = datetime.timedelta(seconds=-standard_time_offset)
444
480
  d = u - datetime.timedelta(seconds=standard_time_offset)
@@ -683,7 +719,7 @@ class WWVBMinute(_WWVBMinute):
683
719
  return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
684
720
 
685
721
  @classmethod
686
- def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None:
722
+ def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None: # noqa: PLR0912
687
723
  """Construct a WWVBMinute from a WWVBTimecode"""
688
724
  for i in (0, 9, 19, 29, 39, 49, 59):
689
725
  if t.am[i] != AmplitudeModulation.MARK:
@@ -698,9 +734,13 @@ class WWVBMinute(_WWVBMinute):
698
734
  minute = t._get_am_bcd(1, 2, 3, 5, 6, 7, 8)
699
735
  if minute is None:
700
736
  return None
737
+ if minute >= 60:
738
+ return None
701
739
  hour = t._get_am_bcd(12, 13, 15, 16, 17, 18)
702
740
  if hour is None:
703
741
  return None
742
+ if hour >= 24:
743
+ return None
704
744
  days = t._get_am_bcd(22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
705
745
  if days is None:
706
746
  return None
@@ -717,7 +757,9 @@ class WWVBMinute(_WWVBMinute):
717
757
  if days > 366 or (not ly and days > 365):
718
758
  return None
719
759
  ls = bool(t.am[56])
720
- dst = _require(t._get_am_bcd(57, 58))
760
+ dst = t._get_am_bcd(57, 58)
761
+ if dst is None:
762
+ return None
721
763
  return cls(year, days, hour, minute, dst, ut1, ls, ly)
722
764
 
723
765
 
@@ -727,10 +769,10 @@ class WWVBMinuteIERS(WWVBMinute):
727
769
  @classmethod
728
770
  def _get_dut1_info(cls, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
729
771
  d = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
730
- return int(round(get_dut1(d) * 10)) * 100, isls(d)
772
+ return round(get_dut1(d) * 10) * 100, isls(d)
731
773
 
732
774
 
733
- def _bcd_bits(n: int) -> Generator[bool, None, None]:
775
+ def _bcd_bits(n: int) -> Generator[bool]:
734
776
  """Return the bcd representation of n, starting with the least significant bit"""
735
777
  while True:
736
778
  d = n % 10
@@ -744,9 +786,13 @@ class AmplitudeModulation(enum.IntEnum):
744
786
  """Constants that describe an Amplitude Modulation value"""
745
787
 
746
788
  ZERO = 0
789
+ """A zero bit (reduced carrier during the first 200ms of the second)"""
747
790
  ONE = 1
791
+ """A one bit (reduced carrier during the first 500ms of the second)"""
748
792
  MARK = 2
793
+ """A mark bit (reduced carrier during the first 800ms of the second)"""
749
794
  UNSET = -1
795
+ """An unset or unknown amplitude modulation value"""
750
796
 
751
797
 
752
798
  @enum.unique
@@ -754,8 +800,11 @@ class PhaseModulation(enum.IntEnum):
754
800
  """Constants that describe a Phase Modulation value"""
755
801
 
756
802
  ZERO = 0
803
+ """A one bit (180° phase shift during the second)"""
757
804
  ONE = 1
805
+ """A zero bit (No phase shift during the second)"""
758
806
  UNSET = -1
807
+ """An unset or unknown phase modulation value"""
759
808
 
760
809
 
761
810
  class WWVBTimecode:
@@ -778,7 +827,10 @@ class WWVBTimecode:
778
827
  The the bits ``self.am[poslist[i]]`` in MSB order are converted from
779
828
  BCD to integer
780
829
  """
781
- pos = reversed(poslist)
830
+ pos = list(poslist)[::-1]
831
+ for p in pos:
832
+ if self.am[p] not in {AmplitudeModulation.ZERO, AmplitudeModulation.ONE}:
833
+ return None
782
834
  val = [bool(self.am[p]) for p in pos]
783
835
  result = 0
784
836
  base = 1
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '5.0.1'
16
- __version_tuple__ = version_tuple = (5, 0, 1)
20
+ __version__ = version = '5.0.3'
21
+ __version_tuple__ = version_tuple = (5, 0, 3)
@@ -0,0 +1 @@
1
+ {"START": "1972-01-01", "OFFSETS_GZ": "H4sIAHTtFGgC/+2aa3LDMAiEL5uHLDuxnN5/pn/aTmfSSiAWhGR9J8gsywJylqVHPtqxZuH/7leeI0fKsGd5EngQ2WisJWKegrThDa6aJFnL0u4wYZkCE2UmSF0U+13vCveStC6JTfQyW3O86HLJf0SvDgy5u4FCI+WVKRuy0KMjJeXoULIvMDmEWgeRxAJtwXquPCIBqbLh/gbfv0mcxk3mHV9tYiATZP8W/zgw2wd5LpJnY+WErI8abJ3opaIW6592+YMbjSsNWQFlNVVtuhjhtQzSUh4MEpOdDrSW6qsUv+O+Dt+XkIONSrUwvWmTsmq5LO9xsZ+EgcDK+MIESDaYmxSxGlgbGOFjBXMjbV7lc6zlmQ0i48oH5P4+vK7i/AHc7tfTXDtffqFi3m6WhApPSTyDvArU5vUDhm7YaNQYGASVbbwLUBtI2PrhSiZNbvCRrtGUGu0GbjDhJ3aLCx5dQFjt0LFovmWB96e6tktqMenoULXajVS3asBibP3kYXrpmZxnsS2Yf2xRPrHbvQ2D9wjfL4C6b4PWV4otW0vWUYkeWE5M8M594oLbxP77xcl4NuBkG0dfM3xOUf/T0GF+ur+J5pljcODEUZkXg6vIdLYy7g3oZU3bPNDnc8qwGdJZMmAurUsRj6tOo95zP6fb9YPWp5OuZ5X7q2DrmsG/VCyTyaREnDRhnUxOzfzzhzuJhuMTQw5vlI1NAAA="}
@@ -52,7 +52,7 @@ def update_iersdata( # noqa: PLR0915
52
52
  offs_str = r["UT1-UTC"]
53
53
  if not offs_str:
54
54
  break
55
- offs = int(round(float(offs_str) * 10))
55
+ offs = round(float(offs_str) * 10)
56
56
  if not offsets:
57
57
  table_start = datetime.date(1858, 11, 17) + datetime.timedelta(jd)
58
58
 
@@ -98,7 +98,7 @@ def update_iersdata( # noqa: PLR0915
98
98
  cells = row.findAll("td")
99
99
  when = datetime.datetime.strptime(cells[0].text + "+0000", "%Y-%m-%d%z").date()
100
100
  dut1 = cells[2].text.replace("s", "").replace(" ", "")
101
- dut1 = int(round(float(dut1) * 10))
101
+ dut1 = round(float(dut1) * 10)
102
102
  if wwvb_dut1 is not None:
103
103
  assert wwvb_start is not None
104
104
  patch(wwvb_start, when, wwvb_dut1)
@@ -6,9 +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 threading
11
- import time
12
11
  from tkinter import Canvas, TclError, Tk
13
12
  from typing import TYPE_CHECKING, Any
14
13
 
@@ -31,7 +30,7 @@ def validate_colors(ctx: Any, param: Any, value: str) -> list[str]: # noqa: ARG
31
30
  app = _app()
32
31
  colors = value.split()
33
32
  if len(colors) not in (2, 3, 4, 6):
34
- raise click.BadParameter(f"Give 2, 3, 4 or 6 colors (not {len(colors)}")
33
+ raise click.BadParameter(f"Give 2, 3, 4 or 6 colors (not {len(colors)})")
35
34
  for c in colors:
36
35
  try:
37
36
  app.winfo_rgb(c)
@@ -53,33 +52,36 @@ DEFAULT_COLORS = "#3c3c3c #3c3c3c #3c3c3c #cc3c3c #88883c #3ccc3c"
53
52
 
54
53
 
55
54
  @click.command
56
- @click.option("--colors", callback=validate_colors, default=DEFAULT_COLORS)
57
- @click.option("--size", default=48)
58
- @click.option("--min-size", default=None)
55
+ @click.option(
56
+ "--colors",
57
+ callback=validate_colors,
58
+ default=DEFAULT_COLORS,
59
+ metavar="COLORS",
60
+ help="2, 3, 4, or 6 Tk color values",
61
+ )
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)")
59
64
  def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: PLR0915
60
65
  """Visualize the WWVB signal in realtime"""
61
66
  if min_size is None:
62
67
  min_size = size
63
68
 
64
- def sleep_deadline(deadline: float) -> None:
65
- """Sleep until a deadline"""
66
- now = time.time()
67
- if deadline > now:
68
- time.sleep(deadline - now)
69
+ def deadline_ms(deadline: datetime.datetime) -> int:
70
+ """Compute the number of ms until a deadline"""
71
+ now = datetime.datetime.now(datetime.timezone.utc)
72
+ return int(max(0, (deadline - now).total_seconds()) * 1000)
69
73
 
70
- def wwvbtick() -> Generator[tuple[float, wwvb.AmplitudeModulation], None, None]:
74
+ def wwvbtick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
71
75
  """Yield consecutive values of the WWVB amplitude signal, going from minute to minute"""
72
- timestamp = time.time() // 60 * 60
76
+ timestamp = datetime.datetime.now(datetime.timezone.utc).replace(second=0, microsecond=0)
73
77
 
74
78
  while True:
75
- tt = time.gmtime(timestamp)
76
- key = tt.tm_year, tt.tm_yday, tt.tm_hour, tt.tm_min
77
- timecode = wwvb.WWVBMinuteIERS(*key).as_timecode()
79
+ timecode = wwvb.WWVBMinuteIERS.from_datetime(timestamp).as_timecode()
78
80
  for i, code in enumerate(timecode.am):
79
- yield timestamp + i, code
80
- timestamp = timestamp + 60
81
+ yield timestamp + datetime.timedelta(seconds=i), code
82
+ timestamp = timestamp + datetime.timedelta(seconds=60)
81
83
 
82
- def wwvbsmarttick() -> Generator[tuple[float, wwvb.AmplitudeModulation], None, None]:
84
+ def wwvbsmarttick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
83
85
  """Yield consecutive values of the WWVB amplitude signal
84
86
 
85
87
  .. but deal with time progressing unexpectedly, such as when the
@@ -90,10 +92,10 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
90
92
  """
91
93
  while True:
92
94
  for stamp, code in wwvbtick():
93
- now = time.time()
94
- if stamp < now - 60:
95
+ now = datetime.datetime.now(datetime.timezone.utc)
96
+ if stamp < now - datetime.timedelta(seconds=60):
95
97
  break
96
- if stamp < now - 1:
98
+ if stamp < now - datetime.timedelta(seconds=1):
97
99
  continue
98
100
  yield stamp, code
99
101
 
@@ -127,18 +129,23 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
127
129
  """Turn the canvas's virtual LED off"""
128
130
  canvas.itemconfigure(circle, fill=colors[i])
129
131
 
130
- def thread_func() -> None:
131
- """Update the canvas virtual LED"""
132
+ def controller_func() -> Generator[int]:
133
+ """Update the canvas virtual LED, yielding the number of ms until the next change"""
132
134
  for stamp, code in wwvbsmarttick():
133
- sleep_deadline(stamp)
135
+ yield deadline_ms(stamp)
134
136
  led_on(code)
135
137
  app.update()
136
- sleep_deadline(stamp + 0.2 + 0.3 * int(code))
138
+ yield deadline_ms(stamp + datetime.timedelta(seconds=0.2 + 0.3 * int(code)))
137
139
  led_off(code)
138
140
  app.update()
139
141
 
140
- thread = threading.Thread(target=thread_func, daemon=True)
141
- thread.start()
142
+ controller = controller_func().__next__
143
+
144
+ def after_func() -> None:
145
+ """Repeatedly run the controller after the desired interval"""
146
+ app.after(controller(), after_func)
147
+
148
+ app.after_idle(after_func)
142
149
  app.mainloop()
143
150
 
144
151
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: wwvb
3
- Version: 5.0.1
3
+ Version: 5.0.3
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
 
@@ -1,6 +1,7 @@
1
1
  .coveragerc
2
2
  .gitignore
3
3
  .pre-commit-config.yaml
4
+ .readthedocs.yaml
4
5
  Makefile
5
6
  README.md
6
7
  adafruit_datetime.pyi
@@ -8,7 +9,6 @@ codecov.yml
8
9
  pyproject.toml
9
10
  requirements-dev.txt
10
11
  requirements.txt
11
- .github/workflows/codeql.yml
12
12
  .github/workflows/cron.yml
13
13
  .github/workflows/release.yml
14
14
  .github/workflows/test.yml
@@ -395,6 +395,38 @@ class WWVBRoundtrip(unittest.TestCase):
395
395
  self.assertEqual(WWVBMinute2k(2070, 1, 1, 0, 0).year, 2070)
396
396
  self.assertEqual(WWVBMinute2k(2099, 1, 1, 0, 0).year, 2099)
397
397
 
398
+ def test_invalid_minute(self) -> None:
399
+ """Check that minute 61 is not valid in an AM timecode"""
400
+ base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
401
+ minute = base_minute.as_timecode()
402
+ minute._put_am_bcd(61, 1, 2, 3, 5, 6, 7, 8) # valid BCD, invalid minute
403
+ decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
404
+ assert decoded_minute is None
405
+
406
+ def test_invalid_hour(self) -> None:
407
+ """Check that hour 25 is not valid in an AM timecode"""
408
+ base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
409
+ minute = base_minute.as_timecode()
410
+ minute._put_am_bcd(29, 12, 13, 15, 16, 17, 18) # valid BCD, invalid hour
411
+ decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
412
+ assert decoded_minute is None
413
+
414
+ def test_invalid_bcd_day(self) -> None:
415
+ """Check that invalid BCD is detected in AM timecode"""
416
+ base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
417
+ minute = base_minute.as_timecode()
418
+ minute.am[30:34] = [wwvb.AmplitudeModulation.ONE] * 4 # invalid BCD 0xf
419
+ decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
420
+ assert decoded_minute is None
421
+
422
+ def test_invalid_mark(self) -> None:
423
+ """Check that invalid presence of MARK in a data field is detected"""
424
+ base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
425
+ minute = base_minute.as_timecode()
426
+ minute.am[57] = wwvb.AmplitudeModulation.MARK
427
+ decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
428
+ assert decoded_minute is None
429
+
398
430
 
399
431
  if __name__ == "__main__":
400
432
  unittest.main()
@@ -1,49 +0,0 @@
1
- # SPDX-FileCopyrightText: 2022-2024 Jeff Epler
2
- #
3
- # SPDX-License-Identifier: CC0-1.0
4
-
5
- name: "CodeQL"
6
-
7
- on:
8
- push:
9
- branches: [ "main" ]
10
- pull_request:
11
- branches: [ "main" ]
12
- schedule:
13
- - cron: "53 3 * * 5"
14
-
15
- jobs:
16
- analyze:
17
- name: Analyze
18
- runs-on: ubuntu-latest
19
- permissions:
20
- actions: read
21
- contents: read
22
- security-events: write
23
-
24
- strategy:
25
- fail-fast: false
26
- matrix:
27
- language: [ python ]
28
-
29
- steps:
30
- - name: Checkout
31
- uses: actions/checkout@v4
32
- with:
33
- persist-credentials: false
34
- - name: Install Dependencies (python)
35
- run: pip3 install -r requirements-dev.txt
36
-
37
- - name: Initialize CodeQL
38
- uses: github/codeql-action/init@v3
39
- with:
40
- languages: ${{ matrix.language }}
41
- queries: +security-and-quality
42
-
43
- - name: Autobuild
44
- uses: github/codeql-action/autobuild@v3
45
-
46
- - name: Perform CodeQL Analysis
47
- uses: github/codeql-action/analyze@v3
48
- with:
49
- category: "/language:${{ matrix.language }}"
wwvb-5.0.1/doc/index.rst DELETED
@@ -1,31 +0,0 @@
1
- .. SPDX-FileCopyrightText: 2021-2024 Jeff Epler
2
- ..
3
- .. SPDX-License-Identifier: GPL-3.0-only
4
-
5
- wwvbpy
6
- ======
7
-
8
- .. image:: https://github.com/jepler/wwvbpy/actions/workflows/test.yml/badge.svg
9
- :target: https://github.com/jepler/wwvbpy/actions/workflows/test.yml
10
- :alt: Test wwvbpy
11
-
12
- .. image:: https://img.shields.io/pypi/v/wwvb
13
- :target: https://pypi.org/project/wwvb
14
- :alt: PyPI
15
-
16
-
17
- .. toctree::
18
- :maxdepth: 2
19
- :caption: Contents:
20
-
21
- wwvb module
22
- ===========
23
-
24
- .. automodule:: wwvb
25
- :members:
26
-
27
- uwwvb module
28
- ============
29
-
30
- .. automodule:: uwwvb
31
- :members:
@@ -1 +0,0 @@
1
- {"START": "1972-01-01", "OFFSETS_GZ": "H4sIAMNkdmcC/+2aa3LDMAiEL5uHLTuxnN5/pn/aTmfSSiAWhGy+E2SWZQE58zwiH/1YivB/96vMXiIX2Io8CTyIrDSWGqlMRdrpDa6aJFnr0m4wYZkCE2UmSF0V+13vBveStK6JTfQyW3O86HLJf0RvDgy5u4FCI+WVKTsVoUdHzsrRoWRfYHIItZ5EEgu0Beu58EgEpMpO9zf4/s3iNO4y7/hqEwOZIPu3+PuO2T7Ic5E8GxsnZHvUYOtELxW1WP+0yx/caFxpyAooq6lq06UEr+UkLeXOIDPZ6EBrqb5K8Tvu6/B9CdnZqFQL05s2KauWy/IeF/tJGAisjK9MgGyDuUkRq4G1gRE+VjA30uZNPsdantkgMq58QO4fw+sqzj+A2/16mmvnyy9UzDvMktDgKYlnkFeB2rx+wNANG40aA4OgsY03AWoDCVs/XMmkyQ0+0jWaUqPdwA0m/MRuccGjCwirHToWzbcs8P7U1nZZLSYdHapWu5HqVg1YjK2fPEwvPZPzLPUF848tyid2u7dh8B7h+wVQ923Q+kqxZe3JclSSB+YTM3nnHrjgFth/vzgZzw6cbOMYa4bHFPU/DR3mp/ubKM4cgwMnHZW4GFxFprOVcevAKGva6oExn1MOmyGDJQPm0rpU8bjqdOo993O6Xz9ofToZela5vwrWoTn4l4o5CIIaKejCEgSnJv784V8zOZ+rHS1DD00AAA=="}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes