Compare commits

...

300 commits

Author SHA1 Message Date
757599b8a4
Merge pull request #156 from jepler/small-tweaks 2025-08-27 11:27:45 -05:00
7a66438ec9 remove more traces of codecov 2025-08-27 11:24:23 -05:00
779337f8af
Merge pull request #155 from jepler/small-tweaks
testwwvb: better(?) alternative
2025-08-27 09:33:07 -05:00
906cd45d32 testwwvb: better(?) alternative
avoids typing.cast & pyrefly: ignore
2025-08-27 09:17:50 -05:00
a416d2e760
Merge pull request #154 from jepler/small-tweaks 2025-08-27 09:06:49 -05:00
bf0b767a74 Makefile: Default to running all type checkers.
Signed-off-by: Jeff Epler <jepler@gmail.com>
2025-08-27 08:39:30 -05:00
70e0c6fc54 testcli: Kill the last Any. 2025-08-27 08:38:41 -05:00
399fa8618f actions: Check with 3 type checkers
you will certainly not regret checking with 3 type checkers.
2025-08-26 21:47:40 -05:00
7e7294fc23 testwwvb: Allow redundant cast for pyrefly
mypy requires this cast, and pyright at least doesn't object.

Signed-off-by: Jeff Epler <jepler@gmail.com>
2025-08-26 21:46:30 -05:00
af3defa722 wwvbtk: pyrefly can't handle these 2025-08-26 21:46:11 -05:00
03a8c79b5c testuwwvb: Additional annotation helps pyrefly 2025-08-26 21:45:04 -05:00
f4544d8dd6 tests: Remove 'unittest.main'
This seems redundant and makes pyrefly choke.
2025-08-26 21:44:53 -05:00
9c78eedf2f general: improve typing
Almost rid of 'Any' types!
2025-08-26 21:44:19 -05:00
b007518fb0 wwvbtk: Tighten type specification of validate_colors.
Signed-off-by: Jeff Epler <jepler@gmail.com>
2025-08-26 21:23:20 -05:00
7c0c875ea0 general: use 'TYPE_CHECKING = False' 2025-08-26 21:23:20 -05:00
aed190a9a8 pre-commit: Add pyupgrade.
Signed-off-by: Jeff Epler <jepler@gmail.com>
2025-08-26 21:23:20 -05:00
14c182717c
Merge pull request #150 from jepler/pre-commit-ci-update-config 2025-08-26 09:06:53 -05:00
8790b17da6
Merge pull request #153 from jepler/use-dataclass 2025-08-26 09:06:36 -05:00
d480ca9980 wwvb: removeprefix is a str method now 🎉.
Signed-off-by: Jeff Epler <jepler@gmail.com>
2025-08-26 09:02:19 -05:00
665c7d8fac wwvb: Switch from NamedTuple to Dataclass.
Deprecate the `min` property and add the `minute` property.

Signed-off-by: Jeff Epler <jepler@gmail.com>
2025-08-26 09:02:19 -05:00
pre-commit-ci[bot]
ab13d58672
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.12.4 → v0.12.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.10)
2025-08-25 20:55:47 +00:00
2f664d254e
Merge pull request #152 from jepler/coverage-patch-subprocess 2025-08-10 20:07:51 -05:00
f24393e840 this is already excluded 2025-08-10 17:16:35 -05:00
5d484b6eb4 Use coverage 7.10.x "patch=subprocess"
.. so we don't need our own code to make subprocess calling
work.
2025-08-10 17:16:28 -05:00
92e7ff0e3a
Merge pull request #151 from jepler/dunder_new_typing 2025-08-07 11:01:43 -05:00
6114e8ddd4 these methods return Self 2025-08-07 10:54:12 -05:00
cfd49c9cbe work around a pyrefly type checking limitation 2025-08-07 10:51:27 -05:00
6be262e2f4 explain what this test is doing, I couldn't figure it out 2025-08-07 10:51:27 -05:00
6afca61f10 Makefile: Add rules to run pyright & pyrefly 2025-08-07 10:51:27 -05:00
3d796afff1 updateiers: Add a type annotation 2025-08-07 10:21:19 -05:00
711b85ff55 Work around a pyrefly bug 2025-08-07 10:21:19 -05:00
e2ec0d9069 Further typing fixes
wwvbpy is now free of type errors under pyright.
2025-08-07 10:21:19 -05:00
jepler (github actions cron)
aae5cce3be update iersdata 2025-08-02 10:05:31 +00:00
1f6dba6a59
Merge pull request #149 from jepler/dunder_new_typing
WWVBMinute: correct typing of __new__ method
2025-07-26 19:45:45 -05:00
9210edfd70 WWVBMinute: correct typing of __new__ method 2025-07-26 14:22:08 -05:00
6027e274e8
Merge pull request #141 from jepler/pre-commit-ci-update-config 2025-07-24 14:31:43 -05:00
b405a2dcb9
Merge pull request #148 from jepler/upgrade-coverage 2025-07-24 14:31:14 -05:00
acdb968619 require a newer coverage 2025-07-24 14:04:47 -05:00
9c76dc1a33 coverage: move settings into pyproject 2025-07-24 14:03:21 -05:00
10eb0e67d3 Ensure branch coverage of some if main blocks. 2025-07-24 14:03:13 -05:00
pre-commit-ci[bot]
eae4e778de
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.1 → v0.12.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.1...v0.12.4)
2025-07-21 20:49:06 +00:00
jepler (github actions cron)
6ef71b2837 update iersdata 2025-07-02 10:05:59 +00:00
c15b2802ef
Merge pull request #147 from jepler/update-pre-commit 2025-06-28 06:55:23 +02:00
f3b865fafa Update ruff
New ruff is very opinionated about positional arguments that
can take a boolean type. As a result, the signatures of the
following functions have changed:
 * WWVBMinute: ls, ly are keyword-only
 * next_minute, previous_minute: newut1 and newls are keyword-only
 * from_datetime: newut1, newls, and old_time are keyword-only
2025-06-28 06:50:08 +02:00
jepler (github actions cron)
e7abe54821 update iersdata 2025-06-02 10:06:09 +00:00
08e4217636
Merge pull request #142 from jepler/use-upstream-type-hints
Remove adafruit_datetime.pyi
2025-05-27 10:44:39 +02:00
7b0548071f Remove adafruit_datetime.pyi
and depend on correct typing info (+ override to use it). The correct
typing info won't exist until a release of adafruit_datetime with
https://github.com/adafruit/Adafruit_CircuitPython_datetime/pull/33 is
made, likely soon.
2025-05-27 10:42:25 +02:00
7137105b18
Merge pull request #146 from jepler/restore-typechecking 2025-05-21 16:49:58 +02:00
b859900b9b type check the tests too 2025-05-21 16:46:07 +02:00
1bf978445e actually check types with mypy again 2025-05-21 16:42:34 +02:00
5afa43e2c4
Merge pull request #145 from jepler/unpin-sphinx 2025-05-20 15:00:28 +02:00
4e3e36b53d let's readthedocs 2025-05-20 14:52:27 +02:00
ad89db7a31 Doc improvements 2025-05-20 14:49:10 +02:00
882f9d0635 unpin sphinx 2025-05-20 13:52:59 +02:00
adabec27dd
Merge pull request #144 from jepler/remove-codeql-badge 2025-05-20 13:52:09 +02:00
270780ab84 remove codeql badge, we no longer use it 2025-05-20 13:51:43 +02:00
142971bd17
Merge pull request #143 from jepler/split-mypy 2025-05-20 13:50:56 +02:00
703824ff0e split out mypy workflow 2025-05-20 13:44:02 +02:00
jepler (github actions cron)
92a5afdfd5 update iersdata 2025-05-02 16:06:12 +00:00
19a2a23c21
Merge pull request #139 from jepler/pre-commit-ci-update-config 2025-05-02 16:11:26 +02:00
pre-commit-ci[bot]
a23e0fbaaf
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.5 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.5...v0.11.7)
2025-04-28 20:23:30 +00:00
c9a0b64195
Merge pull request #140 from jepler/more-error-detection 2025-04-24 18:35:12 +02:00
ec68ed225f Additional error checking in from_timecode_am
the change from using reversed() is because the pos sequence is now
traversed twice.
2025-04-24 18:31:34 +02:00
3fc237f466 Additional invalid AM timecode tests (failing) 2025-04-24 18:30:53 +02:00
497164e84d
Merge pull request #138 from jepler/test-314
new python versions ahoy
2025-04-21 09:58:24 +02:00
6b57582d3e
Merge pull request #137 from jepler/dst-enum
Use an enum for DST status
2025-04-21 09:18:11 +02:00
33459d3c6b
Merge pull request #136 from jepler/wwvbdatetime-tk
wwvbtk: Improve --help message & internal improvements
2025-04-21 08:56:45 +02:00
8348db3c92 new python versions ahoy 2025-04-21 08:56:22 +02:00
6de67679a5 Use an enum for DST status 2025-04-21 07:56:30 +02:00
66d4b32bf0 Simplify Generator[] annotation 2025-04-21 07:51:40 +02:00
4e0d7a9743 wwvbtk: Use datetime for timekeeping
I don't know if it's *better* but `WWVBMinuteIERS(*key)` rubbed me
the wrong way.
2025-04-21 07:50:44 +02:00
aae3828270 wwvbtk: Improve --help message. 2025-04-20 13:25:31 +02:00
d5c21c07dd
Merge pull request #134 from jepler/disable-codeql
the codeql workflow is not providing me any value, disable it
2025-04-19 09:10:02 +02:00
dc24f55598 the codeql workflow is not providing me any value, disable it 2025-04-19 08:30:23 +02:00
6f0765d13d
Merge pull request #132 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-04-17 13:08:47 -05:00
2d574f6d53
Merge pull request #133 from jepler/dethread-wwvbtk
No need for threading in wwvbtk
2025-04-17 13:02:07 -05:00
16090774ed fix type hints 2025-04-17 19:20:54 +02:00
4d1bfb8002 Remove a diagnostic ruff told me could "cause conflicts with the formatter" 2025-04-17 19:19:10 +02:00
f5314e2a7c improve documentation of --colors
though it's still not super helpful
2025-04-17 19:18:53 +02:00
77cfe3d79a No need for threading here. 2025-04-17 19:18:21 +02:00
pre-commit-ci[bot]
7f2051eaf9
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.1 → v0.11.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.1...v0.11.5)
2025-04-14 20:23:43 +00:00
jepler (github actions cron)
636d2d8282 update iersdata 2025-04-02 10:05:51 +00:00
91cb807104
Merge pull request #131 from jepler/small-fixes
churn to appease ruff & remove debugging print
2025-03-17 16:52:41 -05:00
d7a80f0cd9 remove debugging print 2025-03-17 16:25:30 -05:00
e093019b91 churn to appease ruff 2025-03-17 16:24:42 -05:00
jepler (github actions cron)
b5f9b04991 update iersdata 2025-03-02 10:04:43 +00:00
61b7114c64
Merge pull request #130 from jepler/ci-ubuntu-24
run CI on ubuntu 24.04
2025-02-11 12:33:36 -06:00
956b9a261f run CI on ubuntu 24.04 2025-02-11 12:29:45 -06:00
jepler (github actions cron)
82e253c54b update iersdata 2025-02-02 10:04:43 +00:00
173289447f
Merge pull request #128 from jepler/pre-commit-ci-update-config 2025-01-14 22:12:34 -06:00
pre-commit-ci[bot]
0e3bb7af31
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.9.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.9.1)
2025-01-13 23:34:06 +00:00
jepler (github actions cron)
e118927173 update iersdata 2025-01-02 10:04:51 +00:00
0e1bcb257f
Merge pull request #127 from jepler/pre-commit-ci-update-config 2024-12-23 20:14:23 -06:00
pre-commit-ci[bot]
f32183cd2c
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)
2024-12-23 23:46:29 +00:00
97e79fc71e
Merge pull request #126 from jepler/pre-commit-ci-update-config 2024-12-23 15:59:43 -06:00
pre-commit-ci[bot]
d975dd64c7
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.3)
2024-12-16 23:25:10 +00:00
4b5396432f
Merge pull request #125 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-12-04 16:52:03 -06:00
pre-commit-ci[bot]
8e18db099d
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.0 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.0...v0.8.1)
2024-12-03 00:03:04 +00:00
jepler (github actions cron)
b6797c531e update iersdata 2024-12-02 10:05:59 +00:00
f0a2a4b2e4
Merge pull request #124 from jepler/pre-commit-ci-update-config 2024-11-25 17:27:54 -06:00
pre-commit-ci[bot]
fe8dd909c6 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-11-25 23:24:03 +00:00
pre-commit-ci[bot]
6e45fdb005
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/fsfe/reuse-tool: v4.0.3 → v5.0.2](https://github.com/fsfe/reuse-tool/compare/v4.0.3...v5.0.2)
- [github.com/astral-sh/ruff-pre-commit: v0.7.3 → v0.8.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.3...v0.8.0)
2024-11-25 23:23:57 +00:00
6638b6b156
Merge pull request #123 from jepler/pre-commit-ci-update-config 2024-11-12 09:53:39 -06:00
pre-commit-ci[bot]
dabbcd12c2
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.1 → v0.7.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.1...v0.7.3)
2024-11-11 23:27:00 +00:00
d7a8dbb6db
Merge pull request #122 from jepler/improve-actions 2024-11-07 17:18:43 -06:00
287d2be85d don't touch this file, it's generated 2024-11-03 09:31:56 -06:00
a917ba13d3 Improve some actions 2024-11-03 09:30:02 -06:00
jepler (github actions cron)
13e0da844d update iersdata 2024-11-02 10:04:47 +00:00
c9d5541d9d
Merge pull request #121 from jepler/pre-commit-ci-update-config 2024-10-28 19:53:37 -05:00
pre-commit-ci[bot]
1930e210a8
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.0 → v0.7.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.0...v0.7.1)
2024-10-28 23:54:54 +00:00
efec58cd2d
Merge pull request #120 from jepler/pre-commit-ci-update-config 2024-10-22 09:09:52 -05:00
pre-commit-ci[bot]
ab5f273537
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.7.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.0)
2024-10-21 23:27:35 +00:00
fca2abce99
Merge pull request #119 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-10-08 06:30:39 -05:00
pre-commit-ci[bot]
167f3cfc3b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-10-08 01:07:14 +00:00
pre-commit-ci[bot]
97538c1e76
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.6.8 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.6.9)
2024-10-08 01:06:54 +00:00
jepler (github actions cron)
e8b05c3868 update iersdata 2024-10-02 10:07:14 +00:00
deeb2d4071
Merge pull request #118 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-10-01 06:30:31 -05:00
pre-commit-ci[bot]
56cc957971
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.7 → v0.6.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.7...v0.6.8)
2024-09-30 23:25:56 +00:00
3ed8784fab
Merge pull request #117 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-09-24 09:09:23 -05:00
pre-commit-ci[bot]
34992c7e52
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.5 → v0.6.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.5...v0.6.7)
2024-09-23 23:07:50 +00:00
ab1b8876c7
Merge pull request #115 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-09-18 09:44:46 -05:00
aa645e632c
Merge pull request #116 from jepler/sphinx-doc-8-deps
welcome to doc dependency hell
2024-09-18 09:44:34 -05:00
5b08dac440 OK actually we have to use sphinx 7 as long as we're supporting python 3.9 2024-09-17 08:54:49 -05:00
a2ea3762c4 welcome to doc dependency hell
sphinx-rtd-theme 2.0.0 declares that it is incompatible with sphinx 8.
As a result, with the old requirements-dev.txt, sphinx 8 and
sphinx-rtd-theme 0.mumble are installed, as sphinx-rtd-theme 0.mumble did
NOT declare an explicit incompatibility with sphinx 8.

This pairing emphatically does NOT work, giving the error
`UndefinedError("'style' is undefined")` during doc building.

For all these AWESOME packages, specify a dependency range.
This is sure to prevent future breakage 😉
2024-09-17 08:51:20 -05:00
pre-commit-ci[bot]
8c921387d8
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.5)
2024-09-16 23:00:57 +00:00
18f9ef4edc
Merge pull request #114 from jepler/iersdata-json
store iersdata in json
2024-09-02 22:16:13 -05:00
856f28226d Store iersdata as json
this avoids problems with ruff wanting to format the file,
which occurred with a recent ruff update (ruff doesn't obey
"fmt: off" I guess)
2024-09-02 22:12:36 -05:00
9981ec2f4a fix pypi links 2024-09-02 22:12:17 -05:00
jepler (github actions cron)
378fed2b0a update iersdata 2024-09-02 10:04:53 +00:00
2638ba0b0d
Merge pull request #112 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-27 05:42:59 -05:00
pre-commit-ci[bot]
bebad6d052
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2)
2024-08-26 23:13:41 +00:00
c59494ca9b
Merge pull request #111 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-20 08:30:49 +03:00
pre-commit-ci[bot]
ea42fb57ab [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-08-19 22:50:42 +00:00
pre-commit-ci[bot]
a318226506
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.7 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.7...v0.6.1)
2024-08-19 22:50:35 +00:00
68828a0a91
Merge pull request #110 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-14 08:53:42 +03:00
pre-commit-ci[bot]
b09d19621b
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.5.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.5.7)
2024-08-12 22:50:40 +00:00
d17b994002
Merge pull request #109 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-06 08:34:14 +03:00
pre-commit-ci[bot]
a46ea764b1
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.5 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.5...v0.5.6)
2024-08-05 23:38:19 +00:00
9a3b8b3d61 re-format code with ruff 2024-08-05 19:09:46 +03:00
fbc5382e0e
Merge pull request #108 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-05 16:23:38 +01:00
jepler (github actions cron)
bc23114862 update iersdata 2024-08-02 10:04:46 +00:00
pre-commit-ci[bot]
0667b651c6
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.4 → v0.5.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.4...v0.5.5)
2024-07-29 22:49:35 +00:00
16112951b0
Merge pull request #107 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-07-23 07:07:05 -05:00
pre-commit-ci[bot]
17cb6870c8
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.2 → v0.5.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.2...v0.5.4)
2024-07-22 22:57:00 +00:00
9440023a93
Delete .pylintrc 2024-07-15 19:39:51 -05:00
2709d1f76a
Merge pull request #106 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-07-15 19:39:26 -05:00
pre-commit-ci[bot]
51044be945
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.1 → v0.5.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.1...v0.5.2)
2024-07-15 22:49:54 +00:00
35de52a0cc
Merge pull request #105 from jepler/tidy
add requirements for distribution building
2024-07-14 21:01:27 -05:00
3da4e3874a make sure installed version passes test too 2024-07-14 20:55:23 -05:00
0e4a05cef1 add requirements for distribution building 2024-07-14 16:08:29 -05:00
b25d68b9af
Merge pull request #104 from jepler/tidy
Update copyright years
2024-07-14 16:01:25 -05:00
3ed08f1879 Update copyright years 2024-07-14 15:07:47 -05:00
a5f6c95664
Merge pull request #103 from jepler/tidy
Put coverage ignore patterns in coveragerc
2024-07-14 14:49:59 -05:00
6805c176e2 Don't bother trying sysmon tracer
.. its speed advantages don't help branch coverage, just line coverage.
2024-07-14 14:36:55 -05:00
6de292a092 Use combine(tzinfo=) not combine().replace(tzinfo=)
this was added in 3.6, no reason not to use it
2024-07-14 14:34:50 -05:00
f6ac384455 Put coverage ignore patterns in coveragerc 2024-07-14 14:34:33 -05:00
5c01760bf7
Merge pull request #102 from jepler/tidy
Tidy up various things
2024-07-14 12:15:50 -05:00
77beefcf9e no expectation to cover the TYPE_CHECKING-only code 2024-07-14 12:04:43 -05:00
27d87052de Fix whitespace in one test 2024-07-14 12:00:08 -05:00
c348e71412 Fix some pre-commit errors that crept in 2024-07-14 12:00:01 -05:00
7fce1a230f Move doc-related files out of top level 2024-07-14 11:53:22 -05:00
93bf28a4cc move wwvbgen testcases inside the new test/ directory 2024-07-14 11:52:42 -05:00
87b6236416 Move tests out of distribution 2024-07-14 11:49:02 -05:00
9c278caeda Migrate away from setup.cfg
all these settings can be placed in pyproject
2024-07-14 11:47:14 -05:00
db6e374322
Merge pull request #101 from jepler/build-docs
build docs during CI
2024-07-08 22:00:47 -05:00
2440e88ca9
Merge pull request #100 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-07-08 21:24:50 -05:00
4eb910f460 require sphinx 2024-07-08 21:24:29 -05:00
0a716adb38 doc improvements & make more methods private 2024-07-08 21:22:34 -05:00
b672ddf418 build docs during CI 2024-07-08 21:04:13 -05:00
9b2bc022c6
Merge pull request #99 from jepler/more-ruff
Enable a bunch more ruff diagnostics
2024-07-08 20:56:46 -05:00
ca0691e667 Mark private methods & functions with underscore 2024-07-08 19:12:51 -05:00
3fea11600d Get rid of DateOrDatetime
.. and just require these routines to take Date objects only.
2024-07-08 19:07:14 -05:00
pre-commit-ci[bot]
eeda8b9841
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/fsfe/reuse-tool: v3.1.0a1 → v4.0.3](https://github.com/fsfe/reuse-tool/compare/v3.1.0a1...v4.0.3)
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.1)
2024-07-08 22:22:10 +00:00
7e188089b2 Enable a bunch more ruff diagnostics 2024-07-07 22:06:16 -05:00
4f37a358b9
Merge pull request #98 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-07-03 21:00:55 -05:00
jepler (github actions cron)
e9273d5602 update iersdata 2024-07-02 10:05:06 +00:00
pre-commit-ci[bot]
bb0c99dfb2
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.5.0)
2024-07-02 00:52:40 +00:00
7c73ff1afa
Merge pull request #97 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-06-25 08:29:53 -05:00
pre-commit-ci[bot]
1ef4a18524
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.9 → v0.4.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.9...v0.4.10)
2024-06-24 22:43:49 +00:00
82c5bf0c7a
Merge pull request #96 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-06-18 09:27:21 -05:00
pre-commit-ci[bot]
47ba9ab368
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.9)
2024-06-17 22:57:32 +00:00
73b71ce9de
Merge pull request #95 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-06-14 07:00:15 -05:00
pre-commit-ci[bot]
934adfbce2
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/fsfe/reuse-tool: v3.0.2 → v3.1.0a1](https://github.com/fsfe/reuse-tool/compare/v3.0.2...v3.1.0a1)
- [github.com/astral-sh/ruff-pre-commit: v0.4.5 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.5...v0.4.8)
2024-06-10 22:53:33 +00:00
jepler (github actions cron)
26da7328ab update iersdata 2024-06-02 10:04:25 +00:00
b9394a1ee6
Merge pull request #94 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-05-27 22:16:22 -04:00
pre-commit-ci[bot]
593055d083
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.4.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.4.5)
2024-05-27 22:22:26 +00:00
a958c70eee
Merge pull request #93 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-05-13 20:02:48 -05:00
pre-commit-ci[bot]
ce0d5b7843
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.4.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.4.4)
2024-05-13 22:23:02 +00:00
6d76391268
Merge pull request #92 from jepler/actions-churn
to everything there is a season
2024-05-12 21:21:59 -05:00
671e6657cf ensure unique artifact name 2024-05-12 21:15:37 -05:00
d7212be37d to everything there is a season
churn churn churn
2024-05-12 21:14:38 -05:00
ed25fdd31d
Merge pull request #91 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-05-06 18:01:43 -05:00
pre-commit-ci[bot]
686dd79204
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.2 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.3)
2024-05-06 22:29:45 +00:00
jepler (github actions cron)
a10414cfb4 update iersdata 2024-05-02 10:04:40 +00:00
639cda9a06
Merge pull request #90 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-30 19:46:55 -05:00
pre-commit-ci[bot]
af9f11d693
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.2)
2024-04-29 22:55:11 +00:00
78fbc63f0f
Merge pull request #89 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-22 19:04:41 -05:00
pre-commit-ci[bot]
195a0288af
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.7...v0.4.1)
2024-04-22 22:10:49 +00:00
a30bd17bed
Merge pull request #88 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-15 20:58:08 -05:00
pre-commit-ci[bot]
097bccb72e
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7)
2024-04-15 22:49:33 +00:00
7288a70776
Merge pull request #87 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-09 14:04:28 -05:00
pre-commit-ci[bot]
5c597bbab1
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
- [github.com/fsfe/reuse-tool: v3.0.1 → v3.0.2](https://github.com/fsfe/reuse-tool/compare/v3.0.1...v3.0.2)
2024-04-08 23:02:43 +00:00
fb40986d44
Merge pull request #86 from jepler/linter-fixes
Prevent ruff from formatting iersdata & remove pylint directives altogether
2024-04-03 08:02:38 -05:00
fc7ba2820f Remove pylint directives generally 2024-04-03 07:57:58 -05:00
2ce05f4804 Prevent ruff from formatting iersdata
A ruff update added a blank line before the docstring. Prevent it
by moving the fmt:off directive even earlier.
2024-04-03 07:55:31 -05:00
b1958659a5
Merge pull request #85 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-02 09:35:33 -05:00
jepler (github actions cron)
6dcdcb54a5 update iersdata 2024-04-02 10:04:27 +00:00
pre-commit-ci[bot]
fec212a573
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5)
2024-04-01 22:37:46 +00:00
937f1286cb
Merge pull request #84 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-03-26 07:49:42 -05:00
pre-commit-ci[bot]
fc3fb62c6d
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.3 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.3...v0.3.4)
2024-03-25 22:17:52 +00:00
fb2f214942
Merge pull request #83 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-03-18 19:25:38 -05:00
pre-commit-ci[bot]
531bb18cfa [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-03-18 22:37:53 +00:00
pre-commit-ci[bot]
381990d0ba
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.2 → v0.3.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.2...v0.3.3)
2024-03-18 22:37:47 +00:00
jepler (github actions cron)
0a89731131 update iersdata 2024-03-02 10:04:14 +00:00
723b129f9c
Merge pull request #82 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-02-19 20:11:48 -06:00
pre-commit-ci[bot]
b57f73e1df
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2)
2024-02-20 00:47:38 +00:00
089d9106fd
Merge pull request #81 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-02-12 17:11:07 -08:00
pre-commit-ci[bot]
44cab7ce46
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1)
2024-02-13 00:31:10 +00:00
4669064cca
Merge pull request #80 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-02-05 15:31:19 -06:00
pre-commit-ci[bot]
55f0c8baaf
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0)
2024-02-05 20:50:30 +00:00
jepler (github actions cron)
b818f3a193 update iersdata 2024-02-02 10:04:38 +00:00
ee28541bbd
Merge pull request #79 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-01-22 16:59:25 -06:00
pre-commit-ci[bot]
04072460d7
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/fsfe/reuse-tool: v2.1.0 → v3.0.1](https://github.com/fsfe/reuse-tool/compare/v2.1.0...v3.0.1)
- [github.com/astral-sh/ruff-pre-commit: v0.1.13 → v0.1.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.13...v0.1.14)
2024-01-22 20:39:19 +00:00
bec57e4a22
Merge pull request #78 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-01-15 15:33:40 -06:00
pre-commit-ci[bot]
7ea0245beb
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.1.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.11...v0.1.13)
2024-01-15 20:31:10 +00:00
bd6b3d5335
Merge pull request #77 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-01-11 09:29:02 -06:00
pre-commit-ci[bot]
bccb8e6db8
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.1.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.1.11)
2024-01-08 21:05:56 +00:00
jepler (github actions cron)
44258da992 update iersdata 2024-01-02 10:04:22 +00:00
a61cfb1061
Merge pull request #76 from jepler/coverage-sysmon
test coverage with new 'sysmon' coverage core
2023-12-27 18:55:51 -06:00
5534666f07
Add ability to use specific coverage core 2023-12-27 18:40:57 -06:00
02c06bc96f
Merge pull request #75 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-12-27 18:39:28 -06:00
fa30aa6084
Switch to pypy 3.10 2023-12-27 18:35:19 -06:00
pre-commit-ci[bot]
bc04f0729b
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.8 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.8...v0.1.9)
2023-12-25 21:07:00 +00:00
2c47981bd0
Merge pull request #74 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-12-18 19:19:49 -06:00
pre-commit-ci[bot]
8e13abfc71
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.7 → v0.1.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.7...v0.1.8)
2023-12-18 21:25:31 +00:00
1a03c963c8
Merge pull request #73 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-12-11 15:46:39 -06:00
pre-commit-ci[bot]
b15dba7be2
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.7)
2023-12-11 21:16:54 +00:00
9a708e77ea
Merge pull request #72 from jepler/ruff-linter
Use ruff as linter, instead of black+isort+pylint
2023-12-02 12:44:15 -06:00
dd93bbb88e
avoid use of try/except here 2023-12-02 12:01:45 -06:00
323a75a99a
switch to ruff as linter 2023-12-02 11:43:24 -06:00
93e4598da2
modernize assuming >= 3.9 2023-12-02 11:15:13 -06:00
jepler (github actions cron)
0b2c449d16 update iersdata 2023-12-02 10:04:14 +00:00
27f332352b
Merge pull request #71 from jepler/fix-update-checking
Fix update checking
2023-11-19 10:36:34 -06:00
bae19e36d0
Close coverage gaps 2023-11-19 10:23:00 -06:00
1ba8303f70
Fix type error (not caught by mypy) comparing date and datetime objects 2023-11-19 10:07:32 -06:00
568b952b37
add new failing test for maybe_warn_update typing problem 2023-11-19 10:07:32 -06:00
4e29c36c84
Remove construct deprecated in Python 3.13
.. and get datetime-aware UTC object when generating for "now"
2023-11-19 10:07:32 -06:00
37828dcc2b
Merge pull request #69 from jepler/use-313-alphas
Use Python 3.13 alphas during CI
2023-11-14 08:26:00 -06:00
1566c3f08f
use python 3.13 alphas during CI 2023-11-14 07:23:16 -06:00
a3fc02d97d
Merge pull request #68 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-11-13 15:59:14 -06:00
pre-commit-ci[bot]
2bb292d339
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0)
2023-11-13 21:23:42 +00:00
jepler (github actions cron)
1356658681 update iersdata 2023-11-02 10:04:44 +00:00
d656589d85
Merge pull request #67 from jepler/remove-codecov-io
do not bother with codecov.io, it's unreliable
2023-10-30 18:39:57 +01:00
aa4f4c0e89
do not bother with codecov.io, it's unreliable 2023-10-30 18:30:16 +01:00
81d16e558a
Merge pull request #66 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-10-24 08:39:06 +02:00
pre-commit-ci[bot]
55b9740197
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1)
2023-10-23 21:14:51 +00:00
bf38458a6f
Merge pull request #63 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-10-10 07:49:25 +01:00
pre-commit-ci[bot]
4b17370c68
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1)
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
- [github.com/pycqa/pylint: v2.17.1 → v3.0.1](https://github.com/pycqa/pylint/compare/v2.17.1...v3.0.1)
2023-10-09 21:22:28 +00:00
jepler (github actions cron)
92455e3341 update iersdata 2023-10-02 10:04:27 +00:00
jepler (github actions cron)
88fd8ed57f update iersdata 2023-09-02 10:04:21 +00:00
60fbca0392
Merge pull request #65 from jepler/wwvbtk-args
wwvb: take arguments, allow customizing colors
2023-08-27 15:22:54 -05:00
0f1ff483a6
py3.9 compatible type annotation 2023-08-27 14:28:37 -05:00
d04508b67f
Add types & blacken 2023-08-27 14:28:20 -05:00
6e6557eb5a
Improve pylinting with click 2023-08-27 14:28:20 -05:00
ece3aece15
wwvbtk: Add commandline goodness 2023-08-27 14:28:17 -05:00
jepler (github actions cron)
be5c059af3 update iersdata 2023-08-02 10:04:39 +00:00
97a2a00366
Merge pull request #55 from jepler/test-with-312
test with 3.12 alphas
2023-07-31 21:02:04 -05:00
8c91b10fae
run pre-commit just once 2023-07-31 20:59:18 -05:00
d786140369
try bumping setuptools 2023-07-31 20:52:51 -05:00
653a8fbfef
Drop 3.7, 3.8 support officially
these were long since dropped in CI testing
2023-07-31 20:52:51 -05:00
7b754d4328
test with 3.12 alphas 2023-07-31 20:52:49 -05:00
10e6e81845
pre-commit: update reuse-tool 2023-07-25 07:45:23 -05:00
0a1f55b6a0
click was released with a fix for the annotations problem 2023-07-25 07:45:22 -05:00
ed5a42aa42
Update README.md
remove non-working lines-of-code badge
2023-07-16 08:30:45 -05:00
d9e4ba5209
run make 2023-07-15 22:03:59 -05:00
2613945f7b
restrict coverage to in-tree files
this turns out to be needed again on my local machine, which otherwise
shows coverage under /usr/lib/python3.11/dist-python
2023-07-15 22:01:38 -05:00
00d328b56e
Merge pull request #64 from jepler/update-precommit
update pre-commit versions to stable versions
2023-07-11 10:10:56 -05:00
8b5ffd5d45
pin click 8.1.3 for development
due failure to mypy type-check with 8.1.4:
 * https://github.com/pallets/click/issues/2558

8.1.4 works fine, but for purposes of running mypy during CI, pin
the version that also works with mypy.
2023-07-11 09:33:25 -05:00
46de96e37b
update pre-commit versions to stable versions 2023-07-11 09:21:54 -05:00
jepler (github actions cron)
79b5632286 update iersdata 2023-07-02 10:04:45 +00:00
jepler (github actions cron)
e76dfe0fc7 update iersdata 2023-06-02 10:04:34 +00:00
jepler (github actions cron)
37f9a838a7 update iersdata 2023-05-02 10:04:42 +00:00
jepler (github actions cron)
88553b3eb5 update iersdata 2023-04-02 10:04:24 +00:00
ca347a1ae2
Merge pull request #62 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-03-28 06:51:27 -05:00
pre-commit-ci[bot]
ce48f9135f
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pycqa/pylint: v2.17.0 → v2.17.1](https://github.com/pycqa/pylint/compare/v2.17.0...v2.17.1)
2023-03-28 03:57:13 +00:00
778a301def
Merge pull request #61 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-03-14 10:03:57 -05:00
pre-commit-ci[bot]
c256be2c0b
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pycqa/pylint: v2.16.4 → v2.17.0](https://github.com/pycqa/pylint/compare/v2.16.4...v2.17.0)
2023-03-14 05:10:09 +00:00
44b23ce35b
Merge pull request #60 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-03-07 10:13:47 -06:00
pre-commit-ci[bot]
42c25ee3ac
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pycqa/pylint: v2.16.2 → v2.16.4](https://github.com/pycqa/pylint/compare/v2.16.2...v2.16.4)
2023-03-07 04:44:22 +00:00
jepler (github actions cron)
20f9383e0e update iersdata 2023-03-02 10:04:43 +00:00
127d381bc8
Merge pull request #59 from jepler/no-4am-dst-change
No 4am dst change
2023-02-25 10:19:20 -06:00
bad14b8ee0
Don't check if 4AM is a DST change hour
.. as there are only 3 rows (1AM, 2AM, 3AM) in 'table 9'

Strangely, this does not fix https://github.com/PyCQA/pylint/issues/8293
even though it looks like the underlying cause of 8293 is that static
analysis is showing an out of bounds access into the array.
2023-02-25 09:58:23 -06:00
d8e0d6462d
use latest 3.x for win & mac testing 2023-02-25 09:58:23 -06:00
fbd388f232
Merge pull request #58 from jepler/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-02-14 12:25:42 -06:00
c4ab6fad7e Bump actions versions to fix warnings 2023-02-14 12:17:10 -06:00
34fc40be13 move comment back to right place, blackening moved it wrong 2023-02-14 12:13:18 -06:00
pre-commit-ci[bot]
c0f3028477 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-02-14 04:09:32 +00:00
pre-commit-ci[bot]
3446d74dcc
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
- [github.com/fsfe/reuse-tool: v1.1.0 → v1.1.2](https://github.com/fsfe/reuse-tool/compare/v1.1.0...v1.1.2)
- [github.com/pycqa/pylint: v2.15.5 → v2.16.2](https://github.com/pycqa/pylint/compare/v2.15.5...v2.16.2)
2023-02-14 04:08:05 +00:00
jepler (github actions cron)
ff530a2761 update iersdata 2023-02-02 10:04:28 +00:00
9ec8424e33
Merge pull request #57 from jepler/pre-commit-use-python3
use python3 in pre-commit
2023-01-31 08:13:57 -06:00
67 changed files with 1152 additions and 1446 deletions

View file

@ -1,7 +0,0 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
[run]
omit =
*/site-packages/*
test*.py

View file

@ -1,48 +0,0 @@
# SPDX-FileCopyrightText: 2022 Jeff Epler
#
# SPDX-License-Identifier: CC0-1.0
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: "53 3 * * 5"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ python ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Dependencies (python)
run: pip3 install -r requirements-dev.txt
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"

View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: CC0-1.0
@ -11,7 +11,7 @@ on:
jobs:
update-dut1:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
if: startswith(github.repository, 'jepler/')
steps:
@ -20,10 +20,12 @@ jobs:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v2.2.0
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.10
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: "3.10"
@ -37,8 +39,10 @@ jobs:
run: python -munittest
- name: Commit updates
env:
REPO: ${{ github.repository }}
run: |
git config user.name "${GITHUB_ACTOR} (github actions cron)"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
git remote set-url --push origin https://${GITHUB_ACTOR}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
if git commit -m"update iersdata" src/wwvb/iersdata_dist.py; then git push origin HEAD:main; fi
git remote set-url --push origin "https://${GITHUB_ACTOR}:${{ secrets.GITHUB_TOKEN }}@github.com/$REPO"
if git commit -m"update iersdata" src/wwvb/iersdata.json; then git push origin HEAD:main; fi

View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: CC0-1.0
@ -11,17 +11,19 @@ on:
jobs:
release:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v2.2.0
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.9

View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: CC0-1.0
@ -13,34 +13,40 @@ on:
types: [rerequested]
jobs:
test:
docs:
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install deps
run: python -mpip install -r requirements-dev.txt
- name: Build HTML docs
run: make html
typing:
strategy:
fail-fast: false
matrix:
python-version:
- '3.9'
- '3.10'
- '3.11.0-rc.1 - 3.11'
- 'pypy-3.9'
- '3.13'
os-version:
- 'ubuntu-latest'
include:
- os-version: 'macos-latest'
python-version: '3.10'
- os-version: 'windows-latest'
python-version: '3.10'
runs-on: ${{ matrix.os-version }}
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v2.2.0
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
@ -49,28 +55,79 @@ jobs:
python -mpip install wheel
python -mpip install -r requirements-dev.txt
- name: pre-commit
run: pre-commit run --all
- name: Check stubs
- name: Check stubs with mypy
if: (! startsWith(matrix.python-version, 'pypy-'))
run: make mypy PYTHON=python
- name: Test
- name: Check stubs with pyrefly
if: (! startsWith(matrix.python-version, 'pypy-'))
run: make pyrefly PYTHON=python
- name: Check stubs with pyright
if: (! startsWith(matrix.python-version, 'pypy-'))
run: make pyright PYTHON=python
test:
strategy:
fail-fast: false
matrix:
python-version:
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
- '3.14.0-alpha.0 - 3.14'
os-version:
- 'ubuntu-latest'
include:
- os-version: 'macos-latest'
python-version: '3.x'
- os-version: 'windows-latest'
python-version: '3.x'
- os-version: 'ubuntu-latest'
python-version: 'pypy-3.10'
runs-on: ${{ matrix.os-version }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install deps
run: |
python -mpip install wheel
python -mpip install -r requirements-dev.txt
- name: Coverage
run: make coverage PYTHON=python
- name: Upload Coverage to Codecov
if: always()
uses: codecov/codecov-action@v3
env:
PYTHON: ${{ matrix.python-version }}
with:
env_vars: PYTHON
fail_ci_if_error: true
- name: Test installed version
run: make test_venv PYTHON=python
- name: Upload Coverage as artifact
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: coverage for ${{ matrix.python-version }} on ${{ matrix.os-version }}
path: coverage.xml
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: pre-commit
run: pip install pre-commit && pre-commit run --all

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: CC0-1.0

View file

@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò
# SPDX-FileCopyrightText: 2020-2024 Jeff Epler
#
# SPDX-License-Identifier: Unlicense
@ -6,30 +7,30 @@ default_language_version:
python: python3
repos:
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v6.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
exclude: tests
exclude: src/wwvb/iersdata.json
- id: trailing-whitespace
exclude: tests
exclude: test/wwvbgen_testcases
- repo: https://github.com/fsfe/reuse-tool
rev: v1.1.0
rev: v5.0.2
hooks:
- id: reuse
- repo: https://github.com/pycqa/pylint
rev: v2.15.5
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.10
hooks:
- id: pylint
additional_dependencies: [beautifulsoup4, requests, adafruit-circuitpython-datetime, click, python-dateutil, leapseconddata]
- repo: https://github.com/pycqa/isort
rev: 5.12.0
# Run the linter.
- id: ruff-check
args: [ --fix ]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: isort
name: isort (python)
args: ['--profile', 'black']
- id: pyupgrade
args: [ --py39-plus ]
exclude: src/uwwvb.py # CircuitPython prevaling standard!

View file

@ -1,12 +0,0 @@
#SPDX-FileCopyrightText: 2021 Jeff Epler
#
#SPDX-License-Identifier: GPL-3.0-only
[MASTER]
py-version=3.7
[MESSAGES CONTROL]
disable=duplicate-code,line-too-long,protected-access
[BASIC]
argument-rgx=[a-z][a-z0-9_]*
variable-naming-style=any

17
.readthedocs.yaml Normal file
View file

@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
version: 2
build:
os: ubuntu-lts-latest
tools:
python: "3"
sphinx:
configuration: doc/conf.py
python:
install:
- requirements: requirements-dev.txt

View file

@ -1,73 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -17,22 +17,43 @@ STEPECHO = @echo
endif
PYTHON ?= python3
ifeq ($(OS),Windows_NT)
ENVPYTHON ?= _env/Scripts/python.exe
else
ENVPYTHON ?= _env/bin/python3
endif
.PHONY: default
default: coverage mypy
default: coverage mypy pyright pyrefly
COVERAGE_INCLUDE=--include "src/**/*.py"
.PHONY: coverage
coverage:
$(Q)$(PYTHON) -mcoverage erase
$(Q)env PYTHONPATH=src $(PYTHON) -mcoverage run --branch -p -m unittest discover -s src
$(Q)env PYTHONPATH=src $(PYTHON) -mcoverage run -p -m unittest discover -s test
$(Q)$(PYTHON) -mcoverage combine -q
$(Q)$(PYTHON) -mcoverage html
$(Q)$(PYTHON) -mcoverage xml
$(Q)$(PYTHON) -mcoverage report --fail-under=100
$(Q)$(PYTHON) -mcoverage html $(COVERAGE_INCLUDE)
$(Q)$(PYTHON) -mcoverage xml $(COVERAGE_INCLUDE)
$(Q)$(PYTHON) -mcoverage report --fail-under=100 $(COVERAGE_INCLUDE)
.PHONY: test_venv
test_venv:
$(Q)$(PYTHON) -mvenv --clear _env
$(Q)$(ENVPYTHON) -mpip install .
$(Q)$(ENVPYTHON) -m unittest discover -s test
.PHONY: mypy
mypy:
$(Q)mypy --strict --no-warn-unused-ignores src
$(Q)mypy --strict --no-warn-unused-ignores src test
.PHONY: pyright
pyright:
$(Q)pyright src test
.PHONY: pyrefly
pyrefly:
$(Q)pyrefly check src test
.PHONY: update
update:
@ -45,17 +66,15 @@ update:
# from the environment for the first two.
SPHINXOPTS ?= -a -E -j auto
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
SOURCEDIR = doc
BUILDDIR = _build
# Route particular targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
.PHONY: html
html:
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
$(Q)$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Copyright (C) 2021 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only

View file

@ -1,20 +1,16 @@
<!--
SPDX-FileCopyrightText: 2021 Jeff Epler
SPDX-FileCopyrightText: 2021-2024 Jeff Epler
SPDX-License-Identifier: GPL-3.0-only
-->
[![Test wwvbgen](https://github.com/jepler/wwvbpy/actions/workflows/test.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/jepler/wwvbpy/branch/main/graph/badge.svg?token=Exx0c3Gp65)](https://codecov.io/gh/jepler/wwvbpy)
[![Update DUT1 data](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
[![PyPI](https://img.shields.io/pypi/v/wwvb)](https://pypi.org/project/wwvb)
![Lines of code](https://img.shields.io/tokei/lines/github/jepler/wwvbpy)
[![CodeQL](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml)
[![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)
# Purpose
wwvbpy generates WWVB timecodes for any desired time. These timecodes
may be useful in testing WWVB decoder software.
Python package and command line programs for interacting with WWVB timecodes.
Where possible, wwvbpy uses existing facilities for calendar and time
manipulation (datetime and dateutil).
@ -40,7 +36,7 @@ The package includes:
# Development status
The author (@jepler) occasionally develops and maintains this project, but
The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
issues are not likely to be acted on. They would be interested in adding
co-maintainer(s).
@ -68,7 +64,7 @@ channel.
# Usage
~~~~
Usage: python -m wwvb.gen [OPTIONS] [TIMESPEC]...
Usage: wwvbgen [OPTIONS] [TIMESPEC]...
Generate WWVB timecodes
@ -98,7 +94,7 @@ Options:
For example, to display the leap second that occurred at the end of 1998,
~~~~
$ python wwvbgen.py -m 7 1998 365 23 56
$ wwvbgen -m 7 1998 365 23 56
WWVB timecode: year=98 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1
'98+365 23:56 210100110200100001120011001102010100010200110100121000001002
'98+365 23:57 210100111200100001120011001102010100010200110100121000001002
@ -120,7 +116,7 @@ The letters `a` through `u` represent offsets of -1.0s through +1.0s
in 0.1s increments; `k` represents 0s. (In practice, only a smaller range
of values, typically -0.7s to +0.8s, is seen)
For 2001 through 2019, NIST has published the actual DUT1 values broadcast,
For 2001 through 2024, NIST has published the actual DUT1 values broadcast,
and the date of each change, though it in the format of an HTML
table and not designed for machine readability:
@ -165,7 +161,9 @@ by the [other implementation I know of](http://www.leapsecond.com/tools/wwvb_pm.
# Testing wwvbpy
Run the testsuite with `python3 -munittest`. There are several test suites:
Run the testsuite, check coverage & type annotations with `gmake`.
There are several test suites:
* `testwwvb.py`: Check output against expected values. Uses hard coded leap seconds. Tests amplitude and phase data, though the phase testcases are dubious as they were also generated by wwvbpy.
* `testuwwvb.py`: Test the reduced-functionality version against the main version
* `testls.py`: Check the IERS data through 2020-1-1 for expected leap seconds

View file

@ -1,430 +0,0 @@
# SPDX-FileCopyrightText: 2015-2021 Jukka Lehtosalo and contributors
#
# SPDX-License-Identifier: Apache-2.0
import sys
from time import struct_time
from typing import (
AnyStr,
ClassVar,
NamedTuple,
Optional,
SupportsAbs,
Tuple,
Type,
TypeVar,
Union,
overload,
)
_S = TypeVar("_S")
if sys.version_info >= (3,):
_Text = str
else:
_Text = Union[str, unicode]
MINYEAR: int
MAXYEAR: int
class tzinfo:
def tzname(self, dt: Optional[datetime]) -> Optional[str]: ...
def utcoffset(self, dt: Optional[datetime]) -> Optional[timedelta]: ...
def dst(self, dt: Optional[datetime]) -> Optional[timedelta]: ...
def fromutc(self, dt: datetime) -> datetime: ...
if sys.version_info >= (3, 2):
class timezone(tzinfo):
utc: ClassVar[timezone]
min: ClassVar[timezone]
max: ClassVar[timezone]
def __init__(self, offset: timedelta, name: str = ...) -> None: ...
def __hash__(self) -> int: ...
if sys.version_info >= (3, 9):
class _IsoCalendarDate(NamedTuple):
year: int
week: int
weekday: int
_tzinfo = tzinfo
class date:
min: ClassVar[date]
max: ClassVar[date]
resolution: ClassVar[timedelta]
def __new__(cls: Type[_S], year: int, month: int, day: int) -> _S: ...
@classmethod
def fromtimestamp(cls: Type[_S], __timestamp: float) -> _S: ...
@classmethod
def today(cls: Type[_S]) -> _S: ...
@classmethod
def fromordinal(cls: Type[_S], n: int) -> _S: ...
if sys.version_info >= (3, 7):
@classmethod
def fromisoformat(cls: Type[_S], date_string: str) -> _S: ...
if sys.version_info >= (3, 8):
@classmethod
def fromisocalendar(cls: Type[_S], year: int, week: int, day: int) -> _S: ...
@property
def year(self) -> int: ...
@property
def month(self) -> int: ...
@property
def day(self) -> int: ...
def ctime(self) -> str: ...
def strftime(self, fmt: _Text) -> str: ...
if sys.version_info >= (3,):
def __format__(self, fmt: str) -> str: ...
else:
def __format__(self, fmt: AnyStr) -> AnyStr: ...
def isoformat(self) -> str: ...
def timetuple(self) -> struct_time: ...
def toordinal(self) -> int: ...
def replace(self, year: int = ..., month: int = ..., day: int = ...) -> date: ...
def __le__(self, other: date) -> bool: ...
def __lt__(self, other: date) -> bool: ...
def __ge__(self, other: date) -> bool: ...
def __gt__(self, other: date) -> bool: ...
if sys.version_info >= (3, 8):
def __add__(self: _S, other: timedelta) -> _S: ...
def __radd__(self: _S, other: timedelta) -> _S: ...
else:
def __add__(self, other: timedelta) -> date: ...
def __radd__(self, other: timedelta) -> date: ...
@overload
def __sub__(self, other: timedelta) -> date: ...
@overload
def __sub__(self, other: date) -> timedelta: ...
def __hash__(self) -> int: ...
def weekday(self) -> int: ...
def isoweekday(self) -> int: ...
if sys.version_info >= (3, 9):
def isocalendar(self) -> _IsoCalendarDate: ...
else:
def isocalendar(self) -> Tuple[int, int, int]: ...
class time:
min: ClassVar[time]
max: ClassVar[time]
resolution: ClassVar[timedelta]
if sys.version_info >= (3, 6):
def __init__(
self,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
*,
fold: int = ...,
) -> None: ...
else:
def __init__(
self,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
) -> None: ...
@property
def hour(self) -> int: ...
@property
def minute(self) -> int: ...
@property
def second(self) -> int: ...
@property
def microsecond(self) -> int: ...
@property
def tzinfo(self) -> Optional[_tzinfo]: ...
if sys.version_info >= (3, 6):
@property
def fold(self) -> int: ...
def __le__(self, other: time) -> bool: ...
def __lt__(self, other: time) -> bool: ...
def __ge__(self, other: time) -> bool: ...
def __gt__(self, other: time) -> bool: ...
def __hash__(self) -> int: ...
if sys.version_info >= (3, 6):
def isoformat(self, timespec: str = ...) -> str: ...
else:
def isoformat(self) -> str: ...
if sys.version_info >= (3, 7):
@classmethod
def fromisoformat(cls: Type[_S], time_string: str) -> _S: ...
def strftime(self, fmt: _Text) -> str: ...
if sys.version_info >= (3,):
def __format__(self, fmt: str) -> str: ...
else:
def __format__(self, fmt: AnyStr) -> AnyStr: ...
def utcoffset(self) -> Optional[timedelta]: ...
def tzname(self) -> Optional[str]: ...
def dst(self) -> Optional[timedelta]: ...
if sys.version_info >= (3, 6):
def replace(
self,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
*,
fold: int = ...,
) -> time: ...
else:
def replace(
self,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
) -> time: ...
_date = date
_time = time
class timedelta(SupportsAbs[timedelta]):
min: ClassVar[timedelta]
max: ClassVar[timedelta]
resolution: ClassVar[timedelta]
if sys.version_info >= (3, 6):
def __init__(
self,
days: float = ...,
seconds: float = ...,
microseconds: float = ...,
milliseconds: float = ...,
minutes: float = ...,
hours: float = ...,
weeks: float = ...,
*,
fold: int = ...,
) -> None: ...
else:
def __init__(
self,
days: float = ...,
seconds: float = ...,
microseconds: float = ...,
milliseconds: float = ...,
minutes: float = ...,
hours: float = ...,
weeks: float = ...,
) -> None: ...
@property
def days(self) -> int: ...
@property
def seconds(self) -> int: ...
@property
def microseconds(self) -> int: ...
def total_seconds(self) -> float: ...
def __add__(self, other: timedelta) -> timedelta: ...
def __radd__(self, other: timedelta) -> timedelta: ...
def __sub__(self, other: timedelta) -> timedelta: ...
def __rsub__(self, other: timedelta) -> timedelta: ...
def __neg__(self) -> timedelta: ...
def __pos__(self) -> timedelta: ...
def __abs__(self) -> timedelta: ...
def __mul__(self, other: float) -> timedelta: ...
def __rmul__(self, other: float) -> timedelta: ...
@overload
def __floordiv__(self, other: timedelta) -> int: ...
@overload
def __floordiv__(self, other: int) -> timedelta: ...
if sys.version_info >= (3,):
@overload
def __truediv__(self, other: timedelta) -> float: ...
@overload
def __truediv__(self, other: float) -> timedelta: ...
def __mod__(self, other: timedelta) -> timedelta: ...
def __divmod__(self, other: timedelta) -> Tuple[int, timedelta]: ...
else:
@overload
def __div__(self, other: timedelta) -> float: ...
@overload
def __div__(self, other: float) -> timedelta: ...
def __le__(self, other: timedelta) -> bool: ...
def __lt__(self, other: timedelta) -> bool: ...
def __ge__(self, other: timedelta) -> bool: ...
def __gt__(self, other: timedelta) -> bool: ...
def __hash__(self) -> int: ...
class datetime(date):
min: ClassVar[datetime]
max: ClassVar[datetime]
resolution: ClassVar[timedelta]
if sys.version_info >= (3, 6):
def __new__(
cls: Type[_S],
year: int,
month: int,
day: int,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
*,
fold: int = ...,
) -> _S: ...
else:
def __new__(
cls: Type[_S],
year: int,
month: int,
day: int,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
) -> _S: ...
@property
def year(self) -> int: ...
@property
def month(self) -> int: ...
@property
def day(self) -> int: ...
@property
def hour(self) -> int: ...
@property
def minute(self) -> int: ...
@property
def second(self) -> int: ...
@property
def microsecond(self) -> int: ...
@property
def tzinfo(self) -> Optional[_tzinfo]: ...
if sys.version_info >= (3, 6):
@property
def fold(self) -> int: ...
@classmethod
def fromtimestamp(cls: Type[_S], t: float, tz: Optional[_tzinfo] = ...) -> _S: ...
@classmethod
def utcfromtimestamp(cls: Type[_S], t: float) -> _S: ...
@classmethod
def today(cls: Type[_S]) -> _S: ...
@classmethod
def fromordinal(cls: Type[_S], n: int) -> _S: ...
if sys.version_info >= (3, 8):
@classmethod
def now(cls: Type[_S], tz: Optional[_tzinfo] = ...) -> _S: ...
else:
@overload
@classmethod
def now(cls: Type[_S], tz: None = ...) -> _S: ...
@overload
@classmethod
def now(cls, tz: _tzinfo) -> datetime: ...
@classmethod
def utcnow(cls: Type[_S]) -> _S: ...
if sys.version_info >= (3, 6):
@classmethod
def combine(
cls, date: _date, time: _time, tzinfo: Optional[_tzinfo] = ...
) -> datetime: ...
else:
@classmethod
def combine(cls, date: _date, time: _time) -> datetime: ...
if sys.version_info >= (3, 7):
@classmethod
def fromisoformat(cls: Type[_S], date_string: str) -> _S: ...
def strftime(self, fmt: _Text) -> str: ...
if sys.version_info >= (3,):
def __format__(self, fmt: str) -> str: ...
else:
def __format__(self, fmt: AnyStr) -> AnyStr: ...
def toordinal(self) -> int: ...
def timetuple(self) -> struct_time: ...
if sys.version_info >= (3, 3):
def timestamp(self) -> float: ...
def utctimetuple(self) -> struct_time: ...
def date(self) -> _date: ...
def time(self) -> _time: ...
def timetz(self) -> _time: ...
if sys.version_info >= (3, 6):
def replace(
self,
year: int = ...,
month: int = ...,
day: int = ...,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
*,
fold: int = ...,
) -> datetime: ...
else:
def replace(
self,
year: int = ...,
month: int = ...,
day: int = ...,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
tzinfo: Optional[_tzinfo] = ...,
) -> datetime: ...
if sys.version_info >= (3, 8):
def astimezone(self: _S, tz: Optional[_tzinfo] = ...) -> _S: ...
elif sys.version_info >= (3, 3):
def astimezone(self, tz: Optional[_tzinfo] = ...) -> datetime: ...
else:
def astimezone(self, tz: _tzinfo) -> datetime: ...
def ctime(self) -> str: ...
if sys.version_info >= (3, 6):
def isoformat(self, sep: str = ..., timespec: str = ...) -> str: ...
else:
def isoformat(self, sep: str = ...) -> str: ...
@classmethod
def strptime(cls, date_string: _Text, format: _Text) -> datetime: ...
def utcoffset(self) -> Optional[timedelta]: ...
def tzname(self) -> Optional[str]: ...
def dst(self) -> Optional[timedelta]: ...
def __le__(self, other: datetime) -> bool: ... # type: ignore
def __lt__(self, other: datetime) -> bool: ... # type: ignore
def __ge__(self, other: datetime) -> bool: ... # type: ignore
def __gt__(self, other: datetime) -> bool: ... # type: ignore
if sys.version_info >= (3, 8):
def __add__(self: _S, other: timedelta) -> _S: ...
def __radd__(self: _S, other: timedelta) -> _S: ...
else:
def __add__(self, other: timedelta) -> datetime: ...
def __radd__(self, other: timedelta) -> datetime: ...
@overload # type: ignore
def __sub__(self, other: datetime) -> timedelta: ...
@overload
def __sub__(self, other: timedelta) -> datetime: ...
def __hash__(self) -> int: ...
def weekday(self) -> int: ...
def isoweekday(self) -> int: ...
if sys.version_info >= (3, 9):
def isocalendar(self) -> _IsoCalendarDate: ...
else:
def isocalendar(self) -> Tuple[int, int, int]: ...

View file

View file

@ -1,4 +1,4 @@
# pylint: disable=all
# ruff: noqa
# fmt: off
# Configuration file for the Sphinx documentation builder.
#
@ -16,15 +16,17 @@ import os
import re
import subprocess
import sys
import pathlib
sys.path.insert(0, os.path.abspath('.'))
ROOT = pathlib.Path(__file__).absolute().parent.parent
sys.path.insert(0, str(ROOT / "src"))
# -- Project information -----------------------------------------------------
project = 'wwvb'
copyright = '2021, Jeff Epler'
author = 'Jeff Epler'
project = "wwvb"
copyright = "2021, Jeff Epler"
author = "Jeff Epler"
# The full version, including alpha/beta/rc tags
final_version = ""
@ -32,12 +34,12 @@ git_describe = subprocess.run(
["git", "describe", "--tags", "--dirty"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding="utf-8"
encoding="utf-8", check=False,
)
if git_describe.returncode == 0:
git_version = re.search(
r"^\d(?:\.\d){0,2}(?:\-(?:alpha|beta|rc)\.\d+){0,1}",
str(git_describe.stdout)
str(git_describe.stdout),
)
if git_version:
final_version = git_version[0]
@ -53,16 +55,17 @@ version = release = final_version
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
"sphinx.ext.autodoc",
"sphinx_mdinclude",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# 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"]
# -- Options for HTML output -------------------------------------------------
@ -70,16 +73,20 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
autodoc_typehints = "description"
autodoc_class_signature = "separated"
# SPDX-FileCopyrightText: 2021 Jeff Epler
default_role = "any"
intersphinx_mapping = {'py': ('https://docs.python.org/3', None)}
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only

24
doc/index.rst Normal file
View file

@ -0,0 +1,24 @@
.. SPDX-FileCopyrightText: 2021-2024 Jeff Epler
..
.. SPDX-License-Identifier: GPL-3.0-only
wwvbpy |version|
================
.. mdinclude:: ../README.md
.. toctree::
:maxdepth: 2
:caption: Contents:
wwvb module
===========
.. automodule:: wwvb
:members:
uwwvb module
============
.. automodule:: uwwvb
:members:

View file

@ -1,20 +1,56 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
[build-system]
requires = [
"beautifulsoup4",
"click",
"requests",
"platformdirs",
"python-dateutil",
"leapseconddata",
"setuptools>=45",
"setuptools>=68",
"setuptools_scm[toml]>=6.0",
"tzdata",
"wheel",
]
build-backend = "setuptools.build_meta"
[tool.setuptool]
package_dir = {"" = "src"}
include-package-data = true
[tool.setuptools.dynamic]
readme = {file = ["README.md"], content-type="text/markdown"}
dependencies = {file = "requirements.txt"}
[tool.setuptools_scm]
write_to = "src/wwvb/__version__.py"
[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", "E741", "C901", "PLR0911", "PLR2004", "PLR0913", "COM812"]
[tool.ruff]
line-length = 120
[project]
name = "wwvb"
authors = [{name = "Jeff Epler", email = "jepler@gmail.com"}]
description = "Generate WWVB timecodes for any desired time"
dynamic = ["readme","version","dependencies"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Python :: Implementation :: CPython",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
]
requires-python = ">=3.9"
[project.urls]
Source = "https://github.com/jepler/wwvbpy"
Documentation = "https://github.com/jepler/wwvbpy"
[project.scripts]
wwvbgen = "wwvb.gen:main"
wwvbdecode = "wwvb.decode:main"
dut1table = "wwvb.dut1table:main"
updateiers = "wwvb.updateiers:main"
[project.gui-scripts]
wwvbtk = "wwvb.wwvbtk:main"
[[tool.mypy.overrides]]
module = ["adafruit_datetime"]
follow_untyped_imports = true
[tool.coverage.run]
patch=["subprocess"]
branch=true

View file

@ -1,21 +1,29 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
adafruit-circuitpython-datetime
beautifulsoup4
build
click
coverage
coverage >= 7.10.3
mypy; implementation_name=="cpython"
pyright; implementation_name=="cpython"
pyrefly; implementation_name=="cpython"
click>=8.1.5; implementation_name=="cpython"
leapseconddata
platformdirs
pre-commit
python-dateutil
requests; implementation_name=="cpython"
setuptools>=45; implementation_name=="cpython"
setuptools>=68; implementation_name=="cpython"
sphinx
sphinx-autodoc-typehints
sphinx-rtd-theme
sphinx-mdinclude
twine; implementation_name=="cpython"
types-beautifulsoup4
types-python-dateutil
types-requests
types-beautifulsoup4; implementation_name=="cpython"
types-python-dateutil; implementation_name=="cpython"
types-requests; implementation_name=="cpython"
typing-extensions; implementation_name=="cpython"
tzdata
wheel

11
requirements.txt Normal file
View file

@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
adafruit-circuitpython-datetime
beautifulsoup4
click
leapseconddata
platformdirs
python-dateutil
requests
tzdata

View file

@ -1,49 +0,0 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
[metadata]
name = wwvb
author = Jeff Epler
author_email = jepler@gmail.com
description = Generate WWVB timecodes for any desired time
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/jepler/wwvbpy
classifiers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: Implementation :: PyPy
Programming Language :: Python :: Implementation :: CPython
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Operating System :: OS Independent
[options]
package_dir =
=src
packages = wwvb
python_requires = >=3.7
py_modules = uwwvb
install_requires =
adafruit-circuitpython-datetime
beautifulsoup4
click
leapseconddata
platformdirs
python-dateutil
requests
[options.entry_points]
console_scripts =
wwvbgen = wwvb.gen:main
wwvbdecode = wwvb.decode:main
dut1table = wwvb.dut1table:main
updateiers = wwvb.updateiers:main
gui_scripts =
wwvbtk = wwvb.wwvbtk:main
[options.package_data]
wwvb = py.typed

View file

@ -1,8 +1,13 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
"""Implementation of a WWVB state machine & decoder for resource-constrained systems"""
# ruff: noqa: C405, PYI024, FBT001, FBT002
"""Implementation of a WWVB state machine & decoder for resource-constrained systems
This version is intended for use with MicroPython & CircuitPython.
"""
from __future__ import annotations
@ -16,9 +21,7 @@ always_mark = set((0, 9, 19, 29, 39, 49, 59))
always_zero = set((4, 10, 11, 14, 20, 21, 34, 35, 44, 54))
bcd_weights = (1, 2, 4, 8, 10, 20, 40, 80, 100, 200, 400, 800)
WWVBMinute = namedtuple(
"WWVBMinute", ["year", "days", "hour", "minute", "dst", "ut1", "ls", "ly"]
)
WWVBMinute = namedtuple("WWVBMinute", ["year", "days", "hour", "minute", "dst", "ut1", "ls", "ly"])
class WWVBDecoder:
@ -30,7 +33,10 @@ class WWVBDecoder:
self.state = 1
def update(self, value: int) -> list[int] | None:
"""Update the _state machine when a new symbol is received. If a possible complete _minute is received, return it; otherwise, return None"""
"""Update the _state machine when a new symbol is received.
If a possible complete _minute is received, return it; otherwise, return None
"""
result = None
if self.state == 1:
self.minute = []
@ -87,7 +93,7 @@ def get_am_bcd(seq: list[int], *poslist: int) -> int | None:
return result
def decode_wwvb( # pylint: disable=too-many-return-statements
def decode_wwvb(
t: list[int] | None,
) -> WWVBMinute | None:
"""Convert a received minute of wwvb symbols to a WWVBMinute. Returns None if any error is detected."""

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,16 @@
# SPDX-License-Identifier: GPL-3.0-only
"""A stateful decoder of WWVB signals"""
from __future__ import annotations
import sys
from typing import Generator, List, Optional
import wwvb
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Generator
# State 1: Unsync'd
# Marker: State 2
# Other: State 1
@ -20,14 +25,12 @@ import wwvb
# State 4: Decoding a minute, starting in second 1
# Second
always_zero = set((4, 10, 11, 14, 20, 21, 34, 35, 44, 54))
always_zero = {4, 10, 11, 14, 20, 21, 34, 35, 44, 54}
def wwvbreceive() -> Generator[ # pylint: disable=too-many-branches
Optional[wwvb.WWVBTimecode], wwvb.AmplitudeModulation, None
]:
"""A stateful decoder of WWVB signals"""
minute: List[wwvb.AmplitudeModulation] = []
def wwvbreceive() -> Generator[wwvb.WWVBTimecode | None, wwvb.AmplitudeModulation, None]:
"""Decode WWVB signals statefully."""
minute: list[wwvb.AmplitudeModulation] = []
state = 1
value = yield None
@ -40,10 +43,7 @@ def wwvbreceive() -> Generator[ # pylint: disable=too-many-branches
value = yield None
elif state == 2:
if value == wwvb.AmplitudeModulation.MARK:
state = 3
else:
state = 1
state = 3 if value == wwvb.AmplitudeModulation.MARK else 1
value = yield None
elif state == 3:
@ -60,10 +60,7 @@ def wwvbreceive() -> Generator[ # pylint: disable=too-many-branches
elif len(minute) % 10 and value == wwvb.AmplitudeModulation.MARK:
# print("UNEXPECTED MARK")
state = 1
elif (
len(minute) - 1 in always_zero
and value != wwvb.AmplitudeModulation.ZERO
):
elif len(minute) - 1 in always_zero and value != wwvb.AmplitudeModulation.ZERO:
# print("UNEXPECTED NONZERO")
state = 1
elif len(minute) == 60:
@ -92,5 +89,5 @@ def main() -> None:
print(w)
if __name__ == "__main__": # pragma no cover
if __name__ == "__main__":
main()

View file

@ -1,10 +1,11 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
"""Print the table of historical DUT1 values"""
from datetime import timedelta
from itertools import groupby
@ -27,5 +28,5 @@ def main() -> None:
print(date)
if __name__ == "__main__": # pragma no branch
if __name__ == "__main__":
main()

View file

@ -1,38 +1,40 @@
#!/usr/bin/python3
"""A command-line program for generating wwvb timecodes"""
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2011-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
import datetime
import sys
from typing import Any, List, Type
import click
import dateutil.parser
from . import WWVBMinute, WWVBMinuteIERS, print_timecodes, print_timecodes_json, styles
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import WWVBChannel
def parse_timespec( # pylint: disable=unused-argument
ctx: Any, param: Any, value: List[str]
) -> datetime.datetime:
def parse_timespec(ctx: click.Context, param: click.Parameter, value: list[str]) -> datetime.datetime: # noqa: ARG001
"""Parse a time specifier from the commandline"""
try:
if len(value) == 5:
year, month, day, hour, minute = map(int, value)
return datetime.datetime(year, month, day, hour, minute)
return datetime.datetime(year, month, day, hour, minute, tzinfo=datetime.timezone.utc)
if len(value) == 4:
year, yday, hour, minute = map(int, value)
return datetime.datetime(year, 1, 1, hour, minute) + datetime.timedelta(
days=yday - 1
return datetime.datetime(year, 1, 1, hour, minute, tzinfo=datetime.timezone.utc) + datetime.timedelta(
days=yday - 1,
)
if len(value) == 1:
return dateutil.parser.parse(value[0])
if len(value) == 0: # pragma no cover
return datetime.datetime.utcnow()
if len(value) == 0:
return datetime.datetime.now(datetime.timezone.utc)
raise ValueError("Unexpected number of arguments")
except ValueError as e:
raise click.UsageError(f"Could not parse timespec: {e}") from e
@ -68,13 +70,11 @@ def parse_timespec( # pylint: disable=unused-argument
help="Force no leap second at the end of the month (Implies --no-iers)",
)
@click.option("--dut1", "-d", type=int, help="Force the DUT1 value (Implies --no-iers)")
@click.option(
"--minutes", "-m", default=10, help="Number of minutes to show (default: 10)"
)
@click.option("--minutes", "-m", default=10, help="Number of minutes to show (default: 10)")
@click.option(
"--style",
default="default",
type=click.Choice(sorted(["json"] + list(styles.keys()))),
type=click.Choice(sorted(["json", *list(styles.keys())])),
help="Style of output",
)
@click.option(
@ -91,21 +91,21 @@ def parse_timespec( # pylint: disable=unused-argument
help="Modulation to show (default: amplitude)",
)
@click.argument("timespec", type=str, nargs=-1, callback=parse_timespec)
# pylint: disable=too-many-arguments, too-many-locals
def main(
*,
iers: bool,
leap_second: bool,
dut1: int,
minutes: int,
style: str,
channel: str,
channel: WWVBChannel,
all_timecodes: bool,
timespec: datetime.datetime,
) -> None:
"""Generate WWVB timecodes
TIMESPEC: one of "year yday hour minute" or "year month day hour minute", or else the current minute"""
TIMESPEC: one of "year yday hour minute" or "year month day hour minute", or else the current minute
"""
if (leap_second is not None) or (dut1 is not None):
iers = False
@ -113,23 +113,18 @@ def main(
newls = None
if iers:
Constructor: Type[WWVBMinute] = WWVBMinuteIERS
constructor: type[WWVBMinute] = WWVBMinuteIERS
else:
Constructor = WWVBMinute
if dut1 is None:
newut1 = -500 * (leap_second or 0)
else:
newut1 = dut1
constructor = WWVBMinute
newut1 = -500 * (leap_second or 0) if dut1 is None else dut1
newls = bool(leap_second)
w = Constructor.from_datetime(timespec, newls=newls, newut1=newut1)
w = constructor.from_datetime(timespec, newls=newls, newut1=newut1)
if style == "json":
print_timecodes_json(w, minutes, channel, file=sys.stdout)
else:
print_timecodes(
w, minutes, channel, style, all_timecodes=all_timecodes, file=sys.stdout
)
print_timecodes(w, minutes, channel, style, all_timecodes=all_timecodes, file=sys.stdout)
if __name__ == "__main__": # pragma no branch
main() # pylint: disable=no-value-for-parameter
if __name__ == "__main__":
main()

1
src/wwvb/iersdata.json Normal file
View file

@ -0,0 +1 @@
{"START": "1972-01-01", "OFFSETS_GZ": "H4sIAOvijWgC/+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/ubKM4cgwMnHZW4GFxFprOVcevAKGva6oExn1MOmyGDJQPm0rpU8bjqdOo993O6Xz9ofToZela5vwrWoTn4l4o5CIIaKejCEgSnJv784V+zUyyvbb/gE8h8bi3oTQAA"}

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Public domain
SPDX-License-Identifier: CC0-1.0

View file

@ -1,31 +1,37 @@
# -*- python3 -*-
"""Retrieve iers data, possibly from user or site data or from the wwvbpy distribution"""
# Copyright (C) 2021 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2011-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
import binascii
import datetime
import os
import gzip
import importlib.resources
import json
import platformdirs
__all__ = ["DUT1_DATA_START", "DUT1_OFFSETS", "start", "span", "end"]
from .iersdata_dist import DUT1_DATA_START, DUT1_OFFSETS
__all__ = ["DUT1_DATA_START", "DUT1_OFFSETS", "end", "span", "start"]
for location in [
platformdirs.user_data_dir("wwvbpy", "unpythonic.net"),
platformdirs.site_data_dir("wwvbpy", "unpythonic.net"),
]: # pragma no cover
filename = os.path.join(location, "wwvbpy_iersdata.py")
if os.path.exists(filename):
with open(filename, encoding="utf-8") as f:
exec(f.read(), globals(), globals()) # pylint: disable=exec-used
content: dict[str, str] = {"START": "1970-01-01", "OFFSETS_GZ": "H4sIAFNx1mYC/wMAAAAAAAAAAAA="}
path = importlib.resources.files("wwvb") / "iersdata.json"
content = json.loads(path.read_text(encoding="utf-8"))
for location in [ # pragma no cover
platformdirs.user_data_path("wwvbpy", "unpythonic.net"),
platformdirs.site_data_path("wwvbpy", "unpythonic.net"),
]:
path = location / "iersdata.json"
if path.exists():
content = json.loads(path.read_text(encoding="utf-8"))
break
start = datetime.datetime.combine(DUT1_DATA_START, datetime.time()).replace(
tzinfo=datetime.timezone.utc
)
DUT1_DATA_START = datetime.date.fromisoformat(content["START"])
DUT1_OFFSETS = gzip.decompress(binascii.a2b_base64(content["OFFSETS_GZ"])).decode("ascii")
start = datetime.datetime.combine(DUT1_DATA_START, datetime.time(), tzinfo=datetime.timezone.utc)
span = datetime.timedelta(days=len(DUT1_OFFSETS))
end = start + span

View file

@ -1,39 +0,0 @@
# -*- python3 -*-
"""File generated from public data - not subject to copyright"""
# SPDX-FileCopyrightText: Public domain
# SPDX-License-Identifier: CC0-1.0
# fmt: off
# isort: skip_file
# pylint: disable=invalid-name
import datetime
__all__ = ['DUT1_DATA_START', 'DUT1_OFFSETS']
DUT1_DATA_START = datetime.date(1972, 1, 1)
d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s = tuple('defghijklmnopqrs')
DUT1_OFFSETS = str( # 19720101
i*182+s*123+k*30+i*31+s*19+r*31+q*29+p*28+o*30+n*36+m*40 # 19730909
+l*39+k*33+j*31+i*31+h*18+r*19+q*38+p*32+o*31+n*33+m*48+l*45 # 19741010
+k*37+j*33+i*34+h*15+r*22+q*34+p*33+o*34+n*37+m*49+l*45+k*36 # 19751118
+j*32+i*36+h*7+r*28+q*33+p*32+o*30+n*33+m*42+l*42+k*34+j*29 # 19761201
+i*33+h*30+r*6+q*36+p*34+o*31+n*32+m*42+l*51+k*37+j*32+i*33 # 19771231
+h*31+q*32+p*29+o*29+n*30+m*32+l*47+k*47+j*36+i*33+h*32+g*18 # 19790116
+q*16+p*35+o*33+n*32+m*35+l*45+k*51+j*39+i*39+h*38+g*2+q*40 # 19800319
+p*39+o*38+n*43+m*57+l*50+k*39+j*42+i*41+h*43+g*37+f*39+e*39 # 19810719
+o*19+n*62+m*43+l*45+k*48+j*44+i*39+h*44+g*21+q*44+p*48+o*43 # 19821223
+n*41+m*36+l*34+k*34+j*38+i*47+s+r*64+q*50+p*42+o*56+n*57 # 19840517
+m*52+l*100+k*61+j*62+i*66+h*52+g*67+f+p*103+o*56+n*68+m*69 # 19860807
+l*107+k*82+j*72+i*67+h*63+g*113+f*63+e*51+o*11+n*60+m*59 # 19880907
+l*121+k*71+j*71+i*67+h*57+g*93+f*61+e*48+d*12+n*41+m*44 # 19900511
+l*46+k*61+j*66+i*47+h*45+g*15+q*32+p*44+o*41+n*48+m*74+l*49 # 19911129
+k*45+j*44+i*40+h*37+g*38+f*50+e*5+o*60+n*49+m*40+l*40+k*38 # 19930322
+j*38+i*36+h*39+g*25+q*31+p*50+o*41+n*41+m*43+l*41+k*39+j*40 # 19940630
+i*39+s*24+r*57+q*43+p*41+o*39+n*38+m*35+l*37+k*43+j*69+i*44 # 19951124
+h*42+g*37+q*4+p*51+o*45+n*44+m*69+l*70+k*50+j*54+i*53+h*40 # 19970612
+g*49+f*18+p*59+o*53+n*52+m*57+l*48+k*53+j*127+i*70+h*30 # 19990303
+r*62+q*79+p*152+o*82+n*106+m*184+l*125+k*217+j*133+i*252 # 20030402
+h*161+g*392+f*322+e*290+n*116+m*154+l*85+k*83+j*91+i*168 # 20080312
+h*105+g*147+f*105+e*42+o*70+n*91+m*154+l*119+k*84+j*217 # 20110511
+i*126+h*176+g*97+f*91+e*52+o*116+n*98+m*70+l*133+k*91+j*91 # 20140507
+i*77+h*140+g*91+f*84+e*70+d*34+n*72+m*76+l*66+k*53+j*56 # 20160831
+i*105+h*77+g*45+q*25+p*63+o*91+n*154+m*105+l*190+k*118 # 20190501
+j*105+i*807+j*376+k*413+l*115 # 20240106
)

View file

@ -1,8 +1,7 @@
# -*- python -*-
"""A library for WWVB timecodes"""
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only

View file

@ -1,40 +1,33 @@
#!/usr/bin/python3
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
"""Update the DUT1 and LS data based on online sources"""
from __future__ import annotations
import binascii
import csv
import datetime
import gzip
import io
import itertools
import os
import json
import pathlib
from typing import Callable, List, Optional
from typing import Callable
import bs4
import click
import platformdirs
import requests
DIST_PATH = str(pathlib.Path(__file__).parent / "iersdata_dist.py")
DIST_PATH = pathlib.Path(__file__).parent / "iersdata.json"
OLD_TABLE_START: Optional[datetime.date] = None
OLD_TABLE_END: Optional[datetime.date] = None
try:
import wwvb.iersdata_dist
OLD_TABLE_START = wwvb.iersdata_dist.DUT1_DATA_START
OLD_TABLE_END = OLD_TABLE_START + datetime.timedelta(
days=len(wwvb.iersdata_dist.DUT1_OFFSETS) - 1
)
except (ImportError, NameError) as e:
pass
IERS_URL = "https://datacenter.iers.org/data/csv/finals2000A.all.csv"
if os.path.exists("finals2000A.all.csv"):
IERS_URL = "finals2000A.all.csv"
IERS_PATH = pathlib.Path("finals2000A.all.csv")
if IERS_PATH.exists():
IERS_URL = str(IERS_PATH)
print("using local", IERS_URL)
NIST_URL = "https://www.nist.gov/pml/time-and-frequency-division/atomic-standards/leap-second-and-ut1-utc-information"
@ -45,22 +38,22 @@ def _get_text(url: str) -> str:
with requests.get(url, timeout=30) as response:
return response.text
else:
return open(url, encoding="utf-8").read()
return pathlib.Path(url).read_text(encoding="utf-8")
def update_iersdata( # pylint: disable=too-many-locals, too-many-branches, too-many-statements
target_file: str,
def update_iersdata( # noqa: PLR0915
target_path: pathlib.Path,
) -> None:
"""Update iersdata.py"""
offsets: List[int] = []
offsets: list[int] = []
iersdata_text = _get_text(IERS_URL)
table_start: datetime.date | None = None
for r in csv.DictReader(io.StringIO(iersdata_text), delimiter=";"):
jd = float(r["MJD"])
offs_str = r["UT1-UTC"]
if not offs_str:
break
offs = int(round(float(offs_str) * 10))
offs = round(float(offs_str) * 10)
if not offsets:
table_start = datetime.date(1858, 11, 17) + datetime.timedelta(jd)
@ -87,30 +80,29 @@ def update_iersdata( # pylint: disable=too-many-locals, too-many-branches, too-
offsets.append(offs)
assert table_start is not None
wwvb_text = _get_text(NIST_URL)
wwvb_data = bs4.BeautifulSoup(wwvb_text, features="html.parser")
wwvb_dut1_table = wwvb_data.findAll("table")[2]
assert wwvb_dut1_table
meta = wwvb_data.find("meta", property="article:modified_time")
assert isinstance(meta, bs4.Tag)
wwvb_data_stamp = (
datetime.datetime.fromisoformat(meta.attrs["content"])
.replace(tzinfo=None)
.date()
)
wwvb_data_stamp = datetime.datetime.fromisoformat(meta.attrs["content"]).replace(tzinfo=None).date()
def patch(patch_start: datetime.date, patch_end: datetime.date, val: int) -> None:
assert table_start is not None
off_start = (patch_start - table_start).days
off_end = (patch_end - table_start).days
offsets[off_start:off_end] = [val] * (off_end - off_start)
wwvb_dut1: Optional[int] = None
wwvb_start: Optional[datetime.date] = None
wwvb_dut1: int | None = None
wwvb_start: datetime.date | None = None
for row in wwvb_dut1_table.findAll("tr")[1:][::-1]:
cells = row.findAll("td")
when = datetime.datetime.strptime(cells[0].text, "%Y-%m-%d").date()
when = datetime.datetime.strptime(cells[0].text + "+0000", "%Y-%m-%d%z").date()
dut1 = cells[2].text.replace("s", "").replace(" ", "")
dut1 = int(round(float(dut1) * 10))
dut1 = round(float(dut1) * 10)
if wwvb_dut1 is not None:
assert wwvb_start is not None
patch(wwvb_start, when, wwvb_dut1)
@ -130,79 +122,44 @@ def update_iersdata( # pylint: disable=too-many-locals, too-many-branches, too-
assert wwvb_start is not None
patch(wwvb_start, wwvb_data_stamp + datetime.timedelta(days=1), wwvb_dut1)
with open(target_file, "wt", encoding="utf-8") as output:
def code(*args: str) -> None:
"""Print to the output file"""
print(*args, file=output)
code("# -*- python3 -*-")
code('"""File generated from public data - not subject to copyright"""')
code("# SPDX" + "-FileCopyrightText: Public domain")
code("# SPDX" + "-License-Identifier: CC0-1.0")
code("# fmt: off")
code("# isort: skip_file")
code("# pylint: disable=invalid-name")
code("import datetime")
code("__all__ = ['DUT1_DATA_START', 'DUT1_OFFSETS']")
code(f"DUT1_DATA_START = {repr(table_start)}")
c = sorted(chr(ord("a") + ch + 10) for ch in set(offsets))
code(f"{','.join(c)} = tuple({repr(''.join(c))})")
code(
f"DUT1_OFFSETS = str( # {table_start.year:04d}{table_start.month:02d}{table_start.day:02d}"
)
line = ""
j = 0
for val, it in itertools.groupby(offsets):
part = ""
ch = chr(ord("a") + val + 10)
sz = len(list(it))
if j:
part = part + "+"
if sz < 2:
part = part + ch
else:
part = part + f"{ch}*{sz}"
j += sz
if len(line + part) > 60:
d = table_start + datetime.timedelta(j - 1)
code(f" {line:<60s} # {d.year:04d}{d.month:02d}{d.day:02d}")
line = part
else:
line = line + part
d = table_start + datetime.timedelta(j - 1)
code(f" {line:<60s} # {d.year:04d}{d.month:02d}{d.day:02d}")
code(")")
table_end = table_start + datetime.timedelta(len(offsets) - 1)
if OLD_TABLE_START:
print(f"old iersdata covered {OLD_TABLE_START} .. {OLD_TABLE_END}")
base = ord("a") + 10
offsets_bin = bytes(base + ch for ch in offsets)
target_path.write_text(
json.dumps(
{
"START": table_start.isoformat(),
"OFFSETS_GZ": binascii.b2a_base64(gzip.compress(offsets_bin)).decode("ascii").strip(),
},
),
)
print(f"iersdata covers {table_start} .. {table_end}")
def iersdata_path(callback: Callable[[str, str], str]) -> str:
def iersdata_path(callback: Callable[[str, str], pathlib.Path]) -> pathlib.Path:
"""Find out the path for this directory"""
return os.path.join(callback("wwvbpy", "unpythonic.net"), "wwvb_iersdata.py")
return callback("wwvbpy", "unpythonic.net") / "iersdata.json"
@click.command()
@click.option(
"--user",
"location",
flag_value=iersdata_path(platformdirs.user_data_dir),
default=iersdata_path(platformdirs.user_data_dir),
flag_value=iersdata_path(platformdirs.user_data_path),
default=iersdata_path(platformdirs.user_data_path),
type=pathlib.Path,
)
@click.option("--dist", "location", flag_value=DIST_PATH)
@click.option(
"--site", "location", flag_value=iersdata_path(platformdirs.site_data_dir)
)
@click.option("--site", "location", flag_value=iersdata_path(platformdirs.site_data_path))
def main(location: str) -> None:
"""Update DUT1 data"""
print("will write to", location)
os.makedirs(os.path.dirname(location), exist_ok=True)
update_iersdata(location)
path = pathlib.Path(location)
print(f"will write to {location!r}")
path.parent.mkdir(parents=True, exist_ok=True)
update_iersdata(path)
if __name__ == "__main__":
main() # pylint: disable=no-value-for-parameter
main()

View file

@ -1,66 +1,112 @@
#!/usr/bin/python3
"""Visualize the WWVB signal in realtime"""
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
import threading
import time
from tkinter import Canvas, Tk # pylint: disable=import-error
from typing import Any, Generator, Tuple
import datetime
import functools
from tkinter import Canvas, Event, TclError, Tk
import click
import wwvb
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Generator
def main() -> None:
@functools.cache
def _app() -> Tk:
"""Create the Tk application object lazily"""
return Tk()
def validate_colors(ctx: click.Context, param: click.Parameter, value: str) -> list[str]: # noqa: ARG001
"""Check that all colors in a string are valid, splitting it to a list"""
app = _app()
colors = value.split()
if len(colors) not in (2, 3, 4, 6):
raise click.BadParameter(f"Give 2, 3, 4 or 6 colors (not {len(colors)})")
for c in colors:
try:
app.winfo_rgb(c)
except TclError as e:
raise click.BadParameter(f"Invalid color {c}") from e
if len(colors) == 2:
off, on = colors
return [off, off, off, on, on, on]
if len(colors) == 3:
return colors + colors
if len(colors) == 4:
off, c1, c2, c3 = colors
return [off, off, off, c1, c2, c3]
return colors
DEFAULT_COLORS = "#3c3c3c #3c3c3c #3c3c3c #cc3c3c #88883c #3ccc3c"
@click.command
@click.option(
"--colors",
callback=validate_colors,
default=DEFAULT_COLORS,
metavar="COLORS",
help="2, 3, 4, or 6 Tk color values",
)
@click.option("--size", default=48, help="initial size in pixels")
@click.option("--min-size", default=None, type=int, help="minimum size in pixels (default: same as initial size)")
def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: PLR0915
"""Visualize the WWVB signal in realtime"""
if min_size is None:
min_size = size
def sleep_deadline(deadline: float) -> None:
"""Sleep until a deadline"""
now = time.time()
if deadline > now:
time.sleep(deadline - now)
def deadline_ms(deadline: datetime.datetime) -> int:
"""Compute the number of ms until a deadline"""
now = datetime.datetime.now(datetime.timezone.utc)
return int(max(0.0, (deadline - now).total_seconds()) * 1000)
def wwvbtick() -> Generator[Tuple[float, wwvb.AmplitudeModulation], None, None]:
def wwvbtick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
"""Yield consecutive values of the WWVB amplitude signal, going from minute to minute"""
timestamp = time.time() // 60 * 60
timestamp = datetime.datetime.now(datetime.timezone.utc).replace(second=0, microsecond=0)
while True:
tt = time.gmtime(timestamp)
key = tt.tm_year, tt.tm_yday, tt.tm_hour, tt.tm_min
timecode = wwvb.WWVBMinuteIERS(*key).as_timecode()
timecode = wwvb.WWVBMinuteIERS.from_datetime(timestamp).as_timecode()
for i, code in enumerate(timecode.am):
yield timestamp + i, code
timestamp = timestamp + 60
yield timestamp + datetime.timedelta(seconds=i), code
timestamp = timestamp + datetime.timedelta(seconds=60)
def wwvbsmarttick() -> Generator[
Tuple[float, wwvb.AmplitudeModulation], None, None
]:
"""Yield consecutive values of the WWVB amplitude signal but deal with time
progressing unexpectedly, such as when the computer is suspended or NTP steps
the clock backwards
def wwvbsmarttick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
"""Yield consecutive values of the WWVB amplitude signal
.. but deal with time progressing unexpectedly, such as when the
computer is suspended or NTP steps the clock backwards
When time goes backwards or advances by more than a minute, get a fresh
wwvbtick object; otherwise, discard time signals more than 1s in the past."""
wwvbtick object; otherwise, discard time signals more than 1s in the past.
"""
while True:
for stamp, code in wwvbtick():
now = time.time()
if stamp < now - 60:
now = datetime.datetime.now(datetime.timezone.utc)
if stamp < now - datetime.timedelta(seconds=60):
break
if stamp < now - 1:
if stamp < now - datetime.timedelta(seconds=1):
continue
yield stamp, code
colors = ["#3c3c3c", "#cc3c3c", "#88883c", "#3ccc3c"]
app = Tk()
app.wm_minsize(48, 48)
canvas = Canvas(app, width=48, height=48, highlightthickness=0)
canvas.pack(fill="both", expand=True)
app = _app()
app.wm_minsize(min_size, min_size)
canvas = Canvas(app, width=size, height=size, highlightthickness=0)
circle = canvas.create_oval(4, 4, 44, 44, outline="black", fill=colors[0])
canvas.pack(fill="both", expand=True)
app.wm_deiconify()
def resize_canvas(event: Any) -> None:
def resize_canvas(event: Event) -> None:
"""Keep the circle filling the window when it is resized"""
sz = min(event.width, event.height) - 8
if sz < 0:
@ -77,24 +123,31 @@ def main() -> None:
def led_on(i: int) -> None:
"""Turn the canvas's virtual LED on"""
canvas.itemconfigure(circle, fill=colors[i + 1])
canvas.itemconfigure(circle, fill=colors[i + 3])
def led_off() -> None:
def led_off(i: int) -> None:
"""Turn the canvas's virtual LED off"""
canvas.itemconfigure(circle, fill=colors[0])
canvas.itemconfigure(circle, fill=colors[i])
def thread_func() -> None:
"""Update the canvas virtual LED"""
def controller_func() -> Generator[int]:
"""Update the canvas virtual LED, yielding the number of ms until the next change"""
for stamp, code in wwvbsmarttick():
sleep_deadline(stamp)
yield deadline_ms(stamp)
led_on(code)
app.update()
sleep_deadline(stamp + 0.2 + 0.3 * int(code))
led_off()
yield deadline_ms(stamp + datetime.timedelta(seconds=0.2 + 0.3 * int(code)))
led_off(code)
app.update()
thread = threading.Thread(target=thread_func, daemon=True)
thread.start()
controller = controller_func().__next__
# pyrefly: ignore # bad-assignment
def after_func() -> None:
"""Repeatedly run the controller after the desired interval"""
app.after(controller(), after_func)
# pyrefly: ignore # bad-argument-type
app.after_idle(after_func)
app.mainloop()

View file

@ -1,55 +1,77 @@
#!/usr/bin/python3
"""Test most wwvblib commandline programs"""
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
# ruff: noqa: N802 D102
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
# pylint: disable=invalid-name
from __future__ import annotations
import json
import os
import subprocess
import sys
import unittest
coverage_add = (
("-m", "coverage", "run", "--branch", "-p") if "COVERAGE_RUN" in os.environ else ()
)
# These imports must remain, even though the module contents are not used directly!
import wwvb.dut1table
import wwvb.gen
# The asserts below are to help prevent their removal by a linter.
assert wwvb.dut1table.__name__ == "wwvb.dut1table"
assert wwvb.gen.__name__ == "wwvb.gen"
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Sequence
from wwvb import JsonMinute
class CLITestCase(unittest.TestCase):
"""Test various CLI commands within wwvbpy"""
def assertProgramOutput(self, expected: str, *args: str) -> None:
"""Check the output from invoking a program matches the expected"""
def programOutput(self, *args: str) -> str:
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
actual = subprocess.check_output(
args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env
)
return subprocess.check_output(args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env)
def moduleArgs(self, *args: str) -> Sequence[str]:
return (sys.executable, "-m", *args)
def moduleOutput(self, *args: str) -> str:
return self.programOutput(sys.executable, "-m", *args)
def assertProgramOutput(self, expected: str, *args: str) -> None:
"""Check the output from invoking a program matches the expected"""
actual = self.programOutput(*args)
self.assertMultiLineEqual(expected, actual, f"args={args}")
def assertProgramOutputStarts(self, expected: str, *args: str) -> None:
"""Check the output from invoking a program matches the expected"""
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
actual = subprocess.check_output(
args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env
)
actual = self.programOutput(*args)
self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}")
def assertModuleOutput(self, expected: str, *args: str) -> None:
"""Check the output from invoking a `python -m modulename` program matches the expected"""
return self.assertProgramOutput(
expected, sys.executable, *coverage_add, "-m", *args
)
actual = self.moduleOutput(*args)
self.assertMultiLineEqual(expected, actual, f"args={args}")
def assertStarts(self, expected: str, actual: str, *args: str) -> None:
self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}")
def assertModuleJson(self, expected: list[JsonMinute], *args: str) -> None:
"""Check the output from invoking a `python -m modulename` program matches the expected"""
actual = self.moduleOutput(*args)
# Note: in mypy, revealed type of json.loads is typing.Any!
self.assertEqual(json.loads(actual), expected)
def assertModuleOutputStarts(self, expected: str, *args: str) -> None:
"""Check the output from invoking a `python -m modulename` program matches the expected"""
return self.assertProgramOutputStarts(
expected, sys.executable, *coverage_add, "-m", *args
)
actual = self.moduleOutput(*args)
self.assertStarts(expected, actual, *args)
def assertProgramError(self, *args: str) -> None:
"""Check the output from invoking a program fails"""
@ -66,10 +88,10 @@ class CLITestCase(unittest.TestCase):
def assertModuleError(self, *args: str) -> None:
"""Check the output from invoking a `python -m modulename` program fails"""
return self.assertProgramError(sys.executable, *coverage_add, "-m", *args)
self.assertProgramError(*self.moduleArgs(*args))
def test_gen(self) -> None:
"""test wwvb.gen"""
"""Test wwvb.gen"""
self.assertModuleOutput(
"""\
WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-200 ly=1 ls=0
@ -153,10 +175,25 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
def test_json(self) -> None:
"""Test the JSON output format"""
self.assertModuleOutput(
"""\
[{"year": 2021, "days": 340, "hour": 3, "minute": 40, "amplitude": "210000000200000001120011001002000000010200010001020001000002", "phase": "111110011011010101000100100110011110001110111010111101001011"}, {"year": 2021, "days": 340, "hour": 3, "minute": 41, "amplitude": "210000001200000001120011001002000000010200010001020001000002", "phase": "001010011100100011000101110000100001101000001111101100000010"}]
""",
self.assertModuleJson(
[
{
"year": 2021,
"days": 340,
"hour": 3,
"minute": 40,
"amplitude": "210000000200000001120011001002000000010200010001020001000002",
"phase": "111110011011010101000100100110011110001110111010111101001011",
},
{
"year": 2021,
"days": 340,
"hour": 3,
"minute": 41,
"amplitude": "210000001200000001120011001002000000010200010001020001000002",
"phase": "001010011100100011000101110000100001101000001111101100000010",
},
],
"wwvb.gen",
"-m",
"2",
@ -166,10 +203,23 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
"both",
"2021-12-6 3:40",
)
self.assertModuleOutput(
"""\
[{"year": 2021, "days": 340, "hour": 3, "minute": 40, "amplitude": "210000000200000001120011001002000000010200010001020001000002"}, {"year": 2021, "days": 340, "hour": 3, "minute": 41, "amplitude": "210000001200000001120011001002000000010200010001020001000002"}]
""",
self.assertModuleJson(
[
{
"year": 2021,
"days": 340,
"hour": 3,
"minute": 40,
"amplitude": "210000000200000001120011001002000000010200010001020001000002",
},
{
"year": 2021,
"days": 340,
"hour": 3,
"minute": 41,
"amplitude": "210000001200000001120011001002000000010200010001020001000002",
},
],
"wwvb.gen",
"-m",
"2",
@ -179,10 +229,23 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
"amplitude",
"2021-12-6 3:40",
)
self.assertModuleOutput(
"""\
[{"year": 2021, "days": 340, "hour": 3, "minute": 40, "phase": "111110011011010101000100100110011110001110111010111101001011"}, {"year": 2021, "days": 340, "hour": 3, "minute": 41, "phase": "001010011100100011000101110000100001101000001111101100000010"}]
""",
self.assertModuleJson(
[
{
"year": 2021,
"days": 340,
"hour": 3,
"minute": 40,
"phase": "111110011011010101000100100110011110001110111010111101001011",
},
{
"year": 2021,
"days": 340,
"hour": 3,
"minute": 41,
"phase": "001010011100100011000101110000100001101000001111101100000010",
},
],
"wwvb.gen",
"-m",
"2",
@ -198,9 +261,11 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
self.assertModuleOutput(
"""\
WWVB timecode: year=2021 days=340 hour=03 min=40 dst=0 ut1=-100 ly=0 ls=0 --style=sextant
2021-340 03:40 🬋🬩🬋🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬩🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬩🬹🬍🬎🬩🬹🬍🬎🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬎🬩🬹🬍🬎🬋🬎🬩🬹🬩🬹🬋🬍🬍🬎🬩🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬋🬎🬩🬹🬋🬩🬩🬹🬍🬎🬩🬹🬋🬹🬩🬹🬍🬎🬩🬹🬋🬎🬩🬹🬋🬩🬩🬹🬩🬹🬍🬎🬋🬹🬍🬎🬍🬎🬩🬹🬍🬎🬩🬹🬋🬩
2021-340 03:40 \
🬋🬩🬋🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬩🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬩🬹🬍🬎🬩🬹🬍🬎🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬎🬩🬹🬍🬎🬋🬎🬩🬹🬩🬹🬋🬍🬍🬎🬩🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬋🬎🬩🬹🬋🬩🬩🬹🬍🬎🬩🬹🬋🬹🬩🬹🬍🬎🬩🬹🬋🬎🬩🬹🬋🬩🬩🬹🬩🬹🬍🬎🬋🬹🬍🬎🬍🬎🬩🬹🬍🬎🬩🬹🬋🬩
2021-340 03:41 🬋🬍🬋🬎🬩🬹🬍🬎🬩🬹🬍🬎🬍🬎🬩🬹🬋🬹🬋🬩🬍🬎🬍🬎🬩🬹🬍🬎🬍🬎🬍🬎🬩🬹🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬹🬩🬹🬩🬹🬋🬎🬍🬎🬍🬎🬋🬍🬩🬹🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬩🬹🬋🬎🬩🬹🬋🬍🬍🬎🬍🬎🬍🬎🬋🬎🬩🬹🬩🬹🬩🬹🬋🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬋🬎🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬋🬍
2021-340 03:41 \
🬋🬍🬋🬎🬩🬹🬍🬎🬩🬹🬍🬎🬍🬎🬩🬹🬋🬹🬋🬩🬍🬎🬍🬎🬩🬹🬍🬎🬍🬎🬍🬎🬩🬹🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬹🬩🬹🬩🬹🬋🬎🬍🬎🬍🬎🬋🬍🬩🬹🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬩🬹🬋🬎🬩🬹🬋🬍🬍🬎🬍🬎🬍🬎🬋🬎🬩🬹🬩🬹🬩🬹🬋🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬋🬎🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬋🬍
""",
"wwvb.gen",
@ -211,6 +276,15 @@ WWVB timecode: year=2021 days=340 hour=03 min=40 dst=0 ut1=-100 ly=0 ls=0 --styl
"2021-12-6 3:40",
)
def test_now(self) -> None:
"""Test outputting timecodes for 'now'"""
self.assertModuleOutputStarts(
"WWVB timecode: year=",
"wwvb.gen",
"-m",
"1",
)
def test_decode(self) -> None:
"""Test the commandline decoder"""
self.assertModuleOutput(

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3
"""Test of daylight saving time calculations"""
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
@ -19,9 +19,7 @@ class TestDaylight(unittest.TestCase):
"""Test that the onset of DST is the same in Mountain and WWVBMinute (which uses ls bits)"""
for h in [8, 9, 10]:
for dm in range(-1441, 1442):
d = datetime.datetime(
2021, 3, 14, h, 0, tzinfo=datetime.timezone.utc
) + datetime.timedelta(minutes=dm)
d = datetime.datetime(2021, 3, 14, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
m = wwvb.WWVBMinute.from_datetime(d)
self.assertEqual(
m.as_datetime_local().replace(tzinfo=Mountain),
@ -32,9 +30,7 @@ class TestDaylight(unittest.TestCase):
"""Test that the end of DST is the same in Mountain and WWVBMinute (which uses ls bits)"""
for h in [7, 8, 9]:
for dm in range(-1441, 1442):
d = datetime.datetime(
2021, 11, 7, h, 0, tzinfo=datetime.timezone.utc
) + datetime.timedelta(minutes=dm)
d = datetime.datetime(2021, 11, 7, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
m = wwvb.WWVBMinute.from_datetime(d)
self.assertEqual(
m.as_datetime_local().replace(tzinfo=Mountain),
@ -45,9 +41,7 @@ class TestDaylight(unittest.TestCase):
"""Test that middle of DST is the same in Mountain and WWVBMinute (which uses ls bits)"""
for h in [7, 8, 9]:
for dm in (-1, 0, 1):
d = datetime.datetime(
2021, 7, 7, h, 0, tzinfo=datetime.timezone.utc
) + datetime.timedelta(minutes=dm)
d = datetime.datetime(2021, 7, 7, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
m = wwvb.WWVBMinute.from_datetime(d)
self.assertEqual(
m.as_datetime_local().replace(tzinfo=Mountain),
@ -58,9 +52,7 @@ class TestDaylight(unittest.TestCase):
"""Test that middle of standard time is the same in Mountain and WWVBMinute (which uses ls bits)"""
for h in [7, 8, 9]:
for dm in (-1, 0, 1):
d = datetime.datetime(
2021, 12, 25, h, 0, tzinfo=datetime.timezone.utc
) + datetime.timedelta(minutes=dm)
d = datetime.datetime(2021, 12, 25, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
m = wwvb.WWVBMinute.from_datetime(d)
self.assertEqual(
m.as_datetime_local().replace(tzinfo=Mountain),

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3
"""Leap seconds tests"""
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
@ -11,8 +11,7 @@ import unittest
import leapseconddata
import wwvb
from . import iersdata
from wwvb import iersdata
ONE_DAY = datetime.timedelta(days=1)
@ -55,11 +54,5 @@ class TestLeapSecond(unittest.TestCase):
leap.append(nm)
else:
assert not our_is_ls
d = datetime.datetime.combine(nm, datetime.time()).replace(
tzinfo=datetime.timezone.utc
)
d = datetime.datetime.combine(nm, datetime.time(), tzinfo=datetime.timezone.utc)
self.assertEqual(leap, bench)
if __name__ == "__main__": # pragma: no cover
unittest.main()

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3
"""Test Phase Modulation Signal"""
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
@ -15,23 +15,9 @@ class TestPhaseModulation(unittest.TestCase):
def test_pm(self) -> None:
"""Compare the generated signal from a reference minute in NIST docs"""
ref_am = (
"2011000002"
"0001001112"
"0001010002"
"0110001012"
"0100000012"
"0010010112"
)
ref_am = "201100000200010011120001010002011000101201000000120010010112"
ref_pm = (
"0011101101"
"0001001000"
"0011001000"
"0110001101"
"0011010001"
"0110110110"
)
ref_pm = "001110110100010010000011001000011000110100110100010110110110"
ref_minute = wwvb.WWVBMinuteIERS(2012, 186, 17, 30, dst=3)
ref_time = ref_minute.as_timecode()
@ -41,7 +27,3 @@ class TestPhaseModulation(unittest.TestCase):
self.assertEqual(ref_am, test_am)
self.assertEqual(ref_pm, test_pm)
if __name__ == "__main__": # pragma: no cover
unittest.main()

View file

@ -1,13 +1,15 @@
#!/usr/bin/python3
"""Test of uwwvb.py"""
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
# ruff: noqa: N802
import datetime
import random
import sys
import unittest
import zoneinfo
from typing import Union
import adafruit_datetime
@ -21,10 +23,11 @@ EitherDatetimeOrNone = Union[None, datetime.datetime, adafruit_datetime.datetime
class WWVBRoundtrip(unittest.TestCase):
"""tests of uwwvb.py"""
def assertDateTimeEqualExceptTzInfo( # pylint: disable=invalid-name
self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone
) -> None:
"""Test two datetime objects for equality, excluding tzinfo, and allowing adafruit_datetime and core datetime modules to compare equal"""
def assertDateTimeEqualExceptTzInfo(self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone) -> None:
"""Test two datetime objects for equality
This equality test excludes tzinfo, and allows adafruit_datetime and core datetime modules to compare equal
"""
assert a
assert b
self.assertEqual(
@ -34,17 +37,17 @@ class WWVBRoundtrip(unittest.TestCase):
def test_decode(self) -> None:
"""Test decoding of some minutes including a leap second.
Each minute must decode and match the primary decoder."""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(2012, 6, 30, 23, 50)
)
Each minute must decode and match the primary decoder.
"""
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
assert minute
decoder = uwwvb.WWVBDecoder()
decoder.update(uwwvb.MARK)
any_leap_second = False
for _ in range(20):
timecode = minute.as_timecode()
decoded = None
decoded: uwwvb.WWVBMinute | None = None
if len(timecode.am) == 61:
any_leap_second = True
for code in timecode.am:
@ -59,43 +62,37 @@ class WWVBRoundtrip(unittest.TestCase):
def test_roundtrip(self) -> None:
"""Test that some big range of times all decode the same as the primary decoder"""
dt = datetime.datetime(2002, 1, 1, 0, 0)
delta = datetime.timedelta(
minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182
)
dt = datetime.datetime(2002, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
delta = datetime.timedelta(minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182)
while dt.year < 2013:
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
assert minute
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
assert decoded
self.assertDateTimeEqualExceptTzInfo(
minute.as_datetime_utc(), uwwvb.as_datetime_utc(decoded)
)
self.assertDateTimeEqualExceptTzInfo(minute.as_datetime_utc(), uwwvb.as_datetime_utc(decoded))
dt = dt + delta
def test_dst(self) -> None:
"""Test of DST as handled by the small decoder"""
for dt in (
datetime.datetime(2021, 3, 14, 8, 59),
datetime.datetime(2021, 3, 14, 9, 00),
datetime.datetime(2021, 3, 14, 9, 1),
datetime.datetime(2021, 3, 15, 8, 59),
datetime.datetime(2021, 3, 15, 9, 00),
datetime.datetime(2021, 3, 15, 9, 1),
datetime.datetime(2021, 11, 7, 8, 59),
datetime.datetime(2021, 11, 7, 9, 00),
datetime.datetime(2021, 11, 7, 9, 1),
datetime.datetime(2021, 11, 8, 8, 59),
datetime.datetime(2021, 11, 8, 9, 00),
datetime.datetime(2021, 11, 8, 9, 1),
datetime.datetime(2021, 7, 7, 9, 1),
datetime.datetime(2021, 3, 14, 8, 59, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 3, 14, 9, 00, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 3, 14, 9, 1, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 3, 15, 8, 59, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 3, 15, 9, 00, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 3, 15, 9, 1, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 11, 7, 8, 59, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 11, 7, 9, 00, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 11, 7, 9, 1, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 11, 8, 8, 59, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 11, 8, 9, 00, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 11, 8, 9, 1, tzinfo=datetime.timezone.utc),
datetime.datetime(2021, 7, 7, 9, 1, tzinfo=datetime.timezone.utc),
):
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
assert decoded
self.assertDateTimeEqualExceptTzInfo(
minute.as_datetime_local(), uwwvb.as_datetime_local(decoded)
)
self.assertDateTimeEqualExceptTzInfo(minute.as_datetime_local(), uwwvb.as_datetime_local(decoded))
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
assert decoded
@ -107,7 +104,7 @@ class WWVBRoundtrip(unittest.TestCase):
def test_noise(self) -> None:
"""Test of the state-machine decoder when faced with pseudorandom noise"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(2012, 6, 30, 23, 50)
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
)
r = random.Random(408)
junk = [
@ -116,12 +113,12 @@ class WWVBRoundtrip(unittest.TestCase):
wwvb.AmplitudeModulation.MARK,
wwvb.AmplitudeModulation.ONE,
wwvb.AmplitudeModulation.ZERO,
]
],
)
for _ in range(480)
]
timecode = minute.as_timecode()
test_input = junk + [wwvb.AmplitudeModulation.MARK] + timecode.am
test_input = [*junk, wwvb.AmplitudeModulation.MARK, *timecode.am]
decoder = uwwvb.WWVBDecoder()
for code in test_input[:-1]:
decoded = decoder.update(code)
@ -142,7 +139,7 @@ class WWVBRoundtrip(unittest.TestCase):
def test_noise2(self) -> None:
"""Test of the full minute decoder with targeted errors to get full coverage"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(2012, 6, 30, 23, 50)
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
)
timecode = minute.as_timecode()
decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am])
@ -179,7 +176,7 @@ class WWVBRoundtrip(unittest.TestCase):
def test_noise3(self) -> None:
"""Test impossible BCD values"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(2012, 6, 30, 23, 50)
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
)
timecode = minute.as_timecode()
@ -205,18 +202,16 @@ class WWVBRoundtrip(unittest.TestCase):
self.assertEqual(str(uwwvb.WWVBDecoder()), "<WWVBDecoder 1 []>")
def test_near_year_bug(self) -> None:
"""Chris's WWVB software had a bug where the hours after UTC
midnight on 12-31 of a leap year would be shown incorrectly. Check that we
don't have that bug."""
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2021, 1, 1, 0, 0))
"""Test for a bug seen in another WWVB implementaiton
.. in which the hours after UTC midnight on 12-31 of a leap year would
be shown incorrectly. Check that we don't have that bug.
"""
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2021, 1, 1, 0, 0, tzinfo=datetime.timezone.utc))
timecode = minute.as_timecode()
decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am])
assert decoded
self.assertDateTimeEqualExceptTzInfo(
datetime.datetime(2020, 12, 31, 17, 00), # Mountain time!
datetime.datetime(2020, 12, 31, 17, 00, tzinfo=zoneinfo.ZoneInfo("America/Denver")), # Mountain time!
uwwvb.as_datetime_local(decoded),
)
if __name__ == "__main__": # pragma no cover
unittest.main()

View file

@ -1,24 +1,24 @@
#!/usr/bin/python3
"""Test most wwvblib functionality"""
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
# SPDX-FileCopyrightText: 2011-2024 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
import copy
import datetime
import glob
import io
import pathlib
import random
import sys
import unittest
from typing import Optional
import uwwvb
import wwvb
from . import decode, iersdata, tz
from wwvb import WWVBChannel, decode, iersdata, tz
class WWVBMinute2k(wwvb.WWVBMinute):
@ -27,18 +27,16 @@ class WWVBMinute2k(wwvb.WWVBMinute):
epoch = 2000
# pylint: disable=too-many-locals
class WWVBTestCase(unittest.TestCase):
"""Test each expected output in tests/. Some outputs are from another program, some are from us"""
"""Test each expected output in wwvbgen_testcases/. Some outputs are from another program, some are from us"""
maxDiff = 131072
def test_cases(self) -> None:
"""Generate a test case for each expected output in tests/"""
for test in glob.glob("tests/*"):
for test in ((pathlib.Path(__file__).parent) / "wwvbgen_testcases").glob("*"):
with self.subTest(test=test):
with open(test, "rt", encoding="utf-8") as f:
text = f.read()
text = test.read_text(encoding="utf-8")
lines = [line for line in text.split("\n") if not line.startswith("#")]
while not lines[0]:
del lines[0]
@ -46,15 +44,19 @@ class WWVBTestCase(unittest.TestCase):
header = lines[0].split()
timestamp = " ".join(header[:10])
options = header[10:]
channel = "amplitude"
channel: WWVBChannel = "amplitude"
style = "default"
for o in options:
if o.startswith("--channel="):
channel = o[10:]
if o == "--channel=both":
channel = "both"
elif o == "--channel=amplitude":
channel = "amplitude"
elif o == "--channel=phase":
channel = "phase"
elif o.startswith("--style="):
style = o[8:]
else: # pragma: no cover
raise ValueError(f"Unknown option {repr(o)}")
else:
raise ValueError(f"Unknown option {o!r}")
num_minutes = len(lines) - 2
if channel == "both":
num_minutes = len(lines) // 3
@ -85,16 +87,14 @@ class WWVBRoundtrip(unittest.TestCase):
def test_decode(self) -> None:
"""Test that a range of minutes including a leap second are correctly decoded by the state-based decoder"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(1992, 6, 30, 23, 50)
)
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
decoder = decode.wwvbreceive()
next(decoder)
decoder.send(wwvb.AmplitudeModulation.MARK)
any_leap_second = False
for _ in range(20):
timecode = minute.as_timecode()
decoded: Optional[wwvb.WWVBTimecode] = None
decoded: wwvb.WWVBTimecode | None = None
if len(timecode.am) == 61:
any_leap_second = True
for code in timecode.am:
@ -111,32 +111,28 @@ class WWVBRoundtrip(unittest.TestCase):
def test_cover_fill_pm_timecode_extended(self) -> None:
"""Get full coverage of the function pm_timecode_extended"""
for dt in (
datetime.datetime(1992, 1, 1),
datetime.datetime(1992, 4, 5),
datetime.datetime(1992, 6, 1),
datetime.datetime(1992, 10, 25),
datetime.datetime(1992, 1, 1, tzinfo=datetime.timezone.utc),
datetime.datetime(1992, 4, 5, tzinfo=datetime.timezone.utc),
datetime.datetime(1992, 6, 1, tzinfo=datetime.timezone.utc),
datetime.datetime(1992, 10, 25, tzinfo=datetime.timezone.utc),
):
for hour in (0, 4, 11):
dt = dt.replace(hour=hour, minute=10)
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
dt1 = dt.replace(hour=hour, minute=10)
minute = wwvb.WWVBMinuteIERS.from_datetime(dt1)
assert minute is not None
timecode = minute.as_timecode().am
assert timecode
def test_roundtrip(self) -> None:
"""Test that a wide of minutes are correctly decoded by the state-based decoder"""
dt = datetime.datetime(1992, 1, 1, 0, 0)
delta = datetime.timedelta(
minutes=915 if sys.implementation.name == "cpython" else 86400 - 915
)
dt = datetime.datetime(1992, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
delta = datetime.timedelta(minutes=915 if sys.implementation.name == "cpython" else 86400 - 915)
while dt.year < 1993:
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
assert minute is not None
timecode = minute.as_timecode().am
assert timecode
decoded_minute: Optional[
wwvb.WWVBMinute
] = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode())
decoded_minute: wwvb.WWVBMinute | None = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode())
assert decoded_minute
decoded = decoded_minute.as_timecode().am
self.assertEqual(
@ -148,9 +144,7 @@ class WWVBRoundtrip(unittest.TestCase):
def test_noise(self) -> None:
"""Test against pseudorandom noise"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(1992, 6, 30, 23, 50)
)
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
r = random.Random(408)
junk = [
r.choice(
@ -158,12 +152,12 @@ class WWVBRoundtrip(unittest.TestCase):
wwvb.AmplitudeModulation.MARK,
wwvb.AmplitudeModulation.ONE,
wwvb.AmplitudeModulation.ZERO,
]
],
)
for _ in range(480)
]
timecode = minute.as_timecode()
test_input = junk + [wwvb.AmplitudeModulation.MARK] + timecode.am
test_input = [*junk, wwvb.AmplitudeModulation.MARK, *timecode.am]
decoder = decode.wwvbreceive()
next(decoder)
for code in test_input[:-1]:
@ -180,9 +174,7 @@ class WWVBRoundtrip(unittest.TestCase):
def test_noise2(self) -> None:
"""Test of the full minute decoder with targeted errors to get full coverage"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(2012, 6, 30, 23, 50)
)
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
timecode = minute.as_timecode()
decoded = wwvb.WWVBMinute.from_timecode_am(timecode)
self.assertIsNotNone(decoded)
@ -217,9 +209,7 @@ class WWVBRoundtrip(unittest.TestCase):
def test_noise3(self) -> None:
"""Test impossible BCD values"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(2012, 6, 30, 23, 50)
)
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
timecode = minute.as_timecode()
for poslist in [
@ -241,16 +231,12 @@ class WWVBRoundtrip(unittest.TestCase):
def test_previous_next_minute(self) -> None:
"""Test that previous minute and next minute are inverses"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(1992, 6, 30, 23, 50)
)
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
self.assertEqual(minute, minute.next_minute().previous_minute())
def test_timecode_str(self) -> None:
"""Test the str() and repr() methods"""
minute = wwvb.WWVBMinuteIERS.from_datetime(
datetime.datetime(1992, 6, 30, 23, 50)
)
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
timecode = minute.as_timecode()
self.assertEqual(
str(timecode),
@ -268,13 +254,14 @@ class WWVBRoundtrip(unittest.TestCase):
sm1 = s - datetime.timedelta(days=1)
self.assertEqual(wwvb.get_dut1(s), wwvb.get_dut1(sm1))
e = iersdata.DUT1_DATA_START + datetime.timedelta(
days=len(iersdata.DUT1_OFFSETS) - 1
)
e = iersdata.DUT1_DATA_START + datetime.timedelta(days=len(iersdata.DUT1_OFFSETS) - 1)
ep1 = e + datetime.timedelta(days=1)
self.assertEqual(wwvb.get_dut1(e), wwvb.get_dut1(ep1))
ep2 = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=340)
wwvb.get_dut1(ep2)
def test_epoch(self) -> None:
"""Test the 1970-to-2069 epoch"""
m = wwvb.WWVBMinute(69, 1, 1, 0, 0)
@ -287,24 +274,17 @@ class WWVBRoundtrip(unittest.TestCase):
def test_fromstring(self) -> None:
"""Test the fromstring() classmethod"""
s = "WWVB timecode: year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
self.assertEqual(
wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)
)
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ls=1"
self.assertEqual(
wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)
)
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
t = "year=1998 days=365 hour=23 min=56 dst=0"
self.assertEqual(
wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)
)
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
def test_from_datetime(self) -> None:
"""Test the from_datetime() classmethod"""
d = datetime.datetime(1998, 12, 31, 23, 56, 0)
d = datetime.datetime(1998, 12, 31, 23, 56, 0, tzinfo=datetime.timezone.utc)
self.assertEqual(
wwvb.WWVBMinuteIERS.from_datetime(d),
wwvb.WWVBMinuteIERS.from_datetime(d, newls=True, newut1=-300),
@ -322,19 +302,18 @@ class WWVBRoundtrip(unittest.TestCase):
wwvb.WWVBMinute(2021, 1, 1, 1, ls=False)
with self.assertRaises(ValueError):
wwvb.WWVBMinute.fromstring(
"year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1"
)
def test_deprecated(self) -> None:
"""Ensure that the 'maybe_warn_update' function is covered"""
with self.assertWarnsRegex(DeprecationWarning, "use ly"):
wwvb.WWVBMinute(2020, 1, 1, 1).is_ly()
wwvb.WWVBMinute.fromstring("year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1")
def test_update(self) -> None:
"""Ensure that the 'maybe_warn_update' function is covered"""
with self.assertWarnsRegex(Warning, "updateiers"):
wwvb._maybe_warn_update(datetime.date(1970, 1, 1))
wwvb._maybe_warn_update(datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc))
def test_deprecated_min(self) -> None:
"""Ensure that the 'maybe_warn_update' function is covered"""
with self.assertWarnsRegex(DeprecationWarning, "min property"):
self.assertEqual(wwvb.WWVBMinute(2021, 1, 1, 1).min, wwvb.WWVBMinute(2021, 1, 1, 1).minute)
def test_undefined(self) -> None:
"""Ensure that the check for unset elements in am works"""
@ -343,37 +322,40 @@ class WWVBRoundtrip(unittest.TestCase):
def test_tz(self) -> None:
"""Get a little more coverage in the dst change functions"""
date, row = wwvb.get_dst_change_date_and_row(datetime.datetime(1960, 1, 1))
date, row = wwvb._get_dst_change_date_and_row(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc))
self.assertIsNone(date)
self.assertIsNone(row)
self.assertIsNone(wwvb.get_dst_change_hour(datetime.datetime(1960, 1, 1)))
self.assertIsNone(wwvb._get_dst_change_hour(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)))
self.assertEqual(wwvb.get_dst_next(datetime.datetime(1960, 1, 1)), 0b000111)
self.assertEqual(wwvb._get_dst_next(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)), 0b000111)
# Cuba followed year-round DST for several years
self.assertEqual(
wwvb.get_dst_next(datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba")),
wwvb._get_dst_next(datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc), tz=tz.ZoneInfo("Cuba")),
0b101111,
)
date, row = wwvb.get_dst_change_date_and_row(
datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba")
date, row = wwvb._get_dst_change_date_and_row(
datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc),
tz=tz.ZoneInfo("Cuba"),
)
self.assertIsNone(date)
self.assertIsNone(row)
# California was weird in 1948
self.assertEqual(
wwvb.get_dst_next(
datetime.datetime(1948, 1, 1), tz=tz.ZoneInfo("America/Los_Angeles")
wwvb._get_dst_next(
datetime.datetime(1948, 1, 1, tzinfo=datetime.timezone.utc),
tz=tz.ZoneInfo("America/Los_Angeles"),
),
0b100011,
)
# Berlin had DST changes on Monday in 1917
self.assertEqual(
wwvb.get_dst_next(
datetime.datetime(1917, 1, 1), tz=tz.ZoneInfo("Europe/Berlin")
wwvb._get_dst_next(
datetime.datetime(1917, 1, 1, tzinfo=datetime.timezone.utc),
tz=tz.ZoneInfo("Europe/Berlin"),
),
0b100011,
)
@ -382,8 +364,9 @@ class WWVBRoundtrip(unittest.TestCase):
# Australia observes DST in the other half of the year compared to the
# Northern hemisphere
self.assertEqual(
wwvb.get_dst_next(
datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Australia/Melbourne")
wwvb._get_dst_next(
datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc),
tz=tz.ZoneInfo("Australia/Melbourne"),
),
0b100011,
)
@ -420,6 +403,34 @@ class WWVBRoundtrip(unittest.TestCase):
self.assertEqual(WWVBMinute2k(2070, 1, 1, 0, 0).year, 2070)
self.assertEqual(WWVBMinute2k(2099, 1, 1, 0, 0).year, 2099)
def test_invalid_minute(self) -> None:
"""Check that minute 61 is not valid in an AM timecode"""
base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
minute = base_minute.as_timecode()
minute._put_am_bcd(61, 1, 2, 3, 5, 6, 7, 8) # valid BCD, invalid minute
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
assert decoded_minute is None
if __name__ == "__main__": # pragma no cover
unittest.main()
def test_invalid_hour(self) -> None:
"""Check that hour 25 is not valid in an AM timecode"""
base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
minute = base_minute.as_timecode()
minute._put_am_bcd(29, 12, 13, 15, 16, 17, 18) # valid BCD, invalid hour
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
assert decoded_minute is None
def test_invalid_bcd_day(self) -> None:
"""Check that invalid BCD is detected in AM timecode"""
base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
minute = base_minute.as_timecode()
minute.am[30:34] = [wwvb.AmplitudeModulation.ONE] * 4 # invalid BCD 0xf
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
assert decoded_minute is None
def test_invalid_mark(self) -> None:
"""Check that invalid presence of MARK in a data field is detected"""
base_minute = wwvb.WWVBMinute(2021, 1, 1, 0, 0)
minute = base_minute.as_timecode()
minute.am[57] = wwvb.AmplitudeModulation.MARK
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
assert decoded_minute is None

View file

@ -32,4 +32,3 @@ WWVB timecode: year=2020 days=001 hour=00 min=00 dst=0 ut1=-200 ly=1 ls=0 --chan
2020-001 00:09 200001001200000000020000000002000100010200100001020000010002
001110110100001100010101000000100000101101010010110000110110

View file

@ -1,7 +1,14 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
#
# SPDX-License-Identifier: CC0-1.0
#
# "For six minutes each half hour, from 1016 and 4046 minutes past each hour,
# one-minute frames are replaced by a special extended time frame. Rather than
# transmitting 35 bits of information in one minute, this transmits 7 bits
# (time of day and DST status only) over 6 minutes, giving 30 times as much
# energy per transmitted bit, a 14.8 dB improvement in the link budget compared
# to the standard one-minute time code." (wikipedia)
#
WWVB timecode: year=2021 days=311 hour=08 min=10 dst=1 ut1=-100 ly=0 ls=0 --channel=phase
2021-311 08:10 010000110100000111110110000001010110111111100110110101010001
2021-311 08:11 001001100111100011101110101111010010110010100111001000110001