Compare commits
300 commits
pre-commit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 757599b8a4 | |||
| 7a66438ec9 | |||
| 779337f8af | |||
| 906cd45d32 | |||
| a416d2e760 | |||
| bf0b767a74 | |||
| 70e0c6fc54 | |||
| 399fa8618f | |||
| 7e7294fc23 | |||
| af3defa722 | |||
| 03a8c79b5c | |||
| f4544d8dd6 | |||
| 9c78eedf2f | |||
| b007518fb0 | |||
| 7c0c875ea0 | |||
| aed190a9a8 | |||
| 14c182717c | |||
| 8790b17da6 | |||
| d480ca9980 | |||
| 665c7d8fac | |||
|
|
ab13d58672 | ||
| 2f664d254e | |||
| f24393e840 | |||
| 5d484b6eb4 | |||
| 92e7ff0e3a | |||
| 6114e8ddd4 | |||
| cfd49c9cbe | |||
| 6be262e2f4 | |||
| 6afca61f10 | |||
| 3d796afff1 | |||
| 711b85ff55 | |||
| e2ec0d9069 | |||
|
|
aae5cce3be | ||
| 1f6dba6a59 | |||
| 9210edfd70 | |||
| 6027e274e8 | |||
| b405a2dcb9 | |||
| acdb968619 | |||
| 9c76dc1a33 | |||
| 10eb0e67d3 | |||
|
|
eae4e778de | ||
|
|
6ef71b2837 | ||
| c15b2802ef | |||
| f3b865fafa | |||
|
|
e7abe54821 | ||
| 08e4217636 | |||
| 7b0548071f | |||
| 7137105b18 | |||
| b859900b9b | |||
| 1bf978445e | |||
| 5afa43e2c4 | |||
| 4e3e36b53d | |||
| ad89db7a31 | |||
| 882f9d0635 | |||
| adabec27dd | |||
| 270780ab84 | |||
| 142971bd17 | |||
| 703824ff0e | |||
|
|
92a5afdfd5 | ||
| 19a2a23c21 | |||
|
|
a23e0fbaaf | ||
| c9a0b64195 | |||
| ec68ed225f | |||
| 3fc237f466 | |||
| 497164e84d | |||
| 6b57582d3e | |||
| 33459d3c6b | |||
| 8348db3c92 | |||
| 6de67679a5 | |||
| 66d4b32bf0 | |||
| 4e0d7a9743 | |||
| aae3828270 | |||
| d5c21c07dd | |||
| dc24f55598 | |||
| 6f0765d13d | |||
| 2d574f6d53 | |||
| 16090774ed | |||
| 4d1bfb8002 | |||
| f5314e2a7c | |||
| 77cfe3d79a | |||
|
|
7f2051eaf9 | ||
|
|
636d2d8282 | ||
| 91cb807104 | |||
| d7a80f0cd9 | |||
| e093019b91 | |||
|
|
b5f9b04991 | ||
| 61b7114c64 | |||
| 956b9a261f | |||
|
|
82e253c54b | ||
| 173289447f | |||
|
|
0e3bb7af31 | ||
|
|
e118927173 | ||
| 0e1bcb257f | |||
|
|
f32183cd2c | ||
| 97e79fc71e | |||
|
|
d975dd64c7 | ||
| 4b5396432f | |||
|
|
8e18db099d | ||
|
|
b6797c531e | ||
| f0a2a4b2e4 | |||
|
|
fe8dd909c6 | ||
|
|
6e45fdb005 | ||
| 6638b6b156 | |||
|
|
dabbcd12c2 | ||
| d7a8dbb6db | |||
| 287d2be85d | |||
| a917ba13d3 | |||
|
|
13e0da844d | ||
| c9d5541d9d | |||
|
|
1930e210a8 | ||
| efec58cd2d | |||
|
|
ab5f273537 | ||
| fca2abce99 | |||
|
|
167f3cfc3b | ||
|
|
97538c1e76 | ||
|
|
e8b05c3868 | ||
| deeb2d4071 | |||
|
|
56cc957971 | ||
| 3ed8784fab | |||
|
|
34992c7e52 | ||
| ab1b8876c7 | |||
| aa645e632c | |||
| 5b08dac440 | |||
| a2ea3762c4 | |||
|
|
8c921387d8 | ||
| 18f9ef4edc | |||
| 856f28226d | |||
| 9981ec2f4a | |||
|
|
378fed2b0a | ||
| 2638ba0b0d | |||
|
|
bebad6d052 | ||
| c59494ca9b | |||
|
|
ea42fb57ab | ||
|
|
a318226506 | ||
| 68828a0a91 | |||
|
|
b09d19621b | ||
| d17b994002 | |||
|
|
a46ea764b1 | ||
| 9a3b8b3d61 | |||
| fbc5382e0e | |||
|
|
bc23114862 | ||
|
|
0667b651c6 | ||
| 16112951b0 | |||
|
|
17cb6870c8 | ||
| 9440023a93 | |||
| 2709d1f76a | |||
|
|
51044be945 | ||
| 35de52a0cc | |||
| 3da4e3874a | |||
| 0e4a05cef1 | |||
| b25d68b9af | |||
| 3ed08f1879 | |||
| a5f6c95664 | |||
| 6805c176e2 | |||
| 6de292a092 | |||
| f6ac384455 | |||
| 5c01760bf7 | |||
| 77beefcf9e | |||
| 27d87052de | |||
| c348e71412 | |||
| 7fce1a230f | |||
| 93bf28a4cc | |||
| 87b6236416 | |||
| 9c278caeda | |||
| db6e374322 | |||
| 2440e88ca9 | |||
| 4eb910f460 | |||
| 0a716adb38 | |||
| b672ddf418 | |||
| 9b2bc022c6 | |||
| ca0691e667 | |||
| 3fea11600d | |||
|
|
eeda8b9841 | ||
| 7e188089b2 | |||
| 4f37a358b9 | |||
|
|
e9273d5602 | ||
|
|
bb0c99dfb2 | ||
| 7c73ff1afa | |||
|
|
1ef4a18524 | ||
| 82c5bf0c7a | |||
|
|
47ba9ab368 | ||
| 73b71ce9de | |||
|
|
934adfbce2 | ||
|
|
26da7328ab | ||
| b9394a1ee6 | |||
|
|
593055d083 | ||
| a958c70eee | |||
|
|
ce0d5b7843 | ||
| 6d76391268 | |||
| 671e6657cf | |||
| d7212be37d | |||
| ed25fdd31d | |||
|
|
686dd79204 | ||
|
|
a10414cfb4 | ||
| 639cda9a06 | |||
|
|
af9f11d693 | ||
| 78fbc63f0f | |||
|
|
195a0288af | ||
| a30bd17bed | |||
|
|
097bccb72e | ||
| 7288a70776 | |||
|
|
5c597bbab1 | ||
| fb40986d44 | |||
| fc7ba2820f | |||
| 2ce05f4804 | |||
| b1958659a5 | |||
|
|
6dcdcb54a5 | ||
|
|
fec212a573 | ||
| 937f1286cb | |||
|
|
fc3fb62c6d | ||
| fb2f214942 | |||
|
|
531bb18cfa | ||
|
|
381990d0ba | ||
|
|
0a89731131 | ||
| 723b129f9c | |||
|
|
b57f73e1df | ||
| 089d9106fd | |||
|
|
44cab7ce46 | ||
| 4669064cca | |||
|
|
55f0c8baaf | ||
|
|
b818f3a193 | ||
| ee28541bbd | |||
|
|
04072460d7 | ||
| bec57e4a22 | |||
|
|
7ea0245beb | ||
| bd6b3d5335 | |||
|
|
bccb8e6db8 | ||
|
|
44258da992 | ||
| a61cfb1061 | |||
| 5534666f07 | |||
| 02c06bc96f | |||
| fa30aa6084 | |||
|
|
bc04f0729b | ||
| 2c47981bd0 | |||
|
|
8e13abfc71 | ||
| 1a03c963c8 | |||
|
|
b15dba7be2 | ||
| 9a708e77ea | |||
| dd93bbb88e | |||
| 323a75a99a | |||
| 93e4598da2 | |||
|
|
0b2c449d16 | ||
| 27f332352b | |||
| bae19e36d0 | |||
| 1ba8303f70 | |||
| 568b952b37 | |||
| 4e29c36c84 | |||
| 37828dcc2b | |||
| 1566c3f08f | |||
| a3fc02d97d | |||
|
|
2bb292d339 | ||
|
|
1356658681 | ||
| d656589d85 | |||
| aa4f4c0e89 | |||
| 81d16e558a | |||
|
|
55b9740197 | ||
| bf38458a6f | |||
|
|
4b17370c68 | ||
|
|
92455e3341 | ||
|
|
88fd8ed57f | ||
| 60fbca0392 | |||
| 0f1ff483a6 | |||
| d04508b67f | |||
| 6e6557eb5a | |||
| ece3aece15 | |||
|
|
be5c059af3 | ||
| 97a2a00366 | |||
| 8c91b10fae | |||
| d786140369 | |||
| 653a8fbfef | |||
| 7b754d4328 | |||
| 10e6e81845 | |||
| 0a1f55b6a0 | |||
| ed5a42aa42 | |||
| d9e4ba5209 | |||
| 2613945f7b | |||
| 00d328b56e | |||
| 8b5ffd5d45 | |||
| 46de96e37b | |||
|
|
79b5632286 | ||
|
|
e76dfe0fc7 | ||
|
|
37f9a838a7 | ||
|
|
88553b3eb5 | ||
| ca347a1ae2 | |||
|
|
ce48f9135f | ||
| 778a301def | |||
|
|
c256be2c0b | ||
| 44b23ce35b | |||
|
|
42c25ee3ac | ||
|
|
20f9383e0e | ||
| 127d381bc8 | |||
| bad14b8ee0 | |||
| d8e0d6462d | |||
| fbd388f232 | |||
| c4ab6fad7e | |||
| 34fc40be13 | |||
|
|
c0f3028477 | ||
|
|
3446d74dcc | ||
|
|
ff530a2761 | ||
| 9ec8424e33 |
67 changed files with 1152 additions and 1446 deletions
|
|
@ -1,7 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2021 Jeff Epler
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
[run]
|
||||
omit =
|
||||
*/site-packages/*
|
||||
test*.py
|
||||
48
.github/workflows/codeql.yml
vendored
48
.github/workflows/codeql.yml
vendored
|
|
@ -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 }}"
|
||||
16
.github/workflows/cron.yml
vendored
16
.github/workflows/cron.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
123
.github/workflows/test.yml
vendored
123
.github/workflows/test.yml
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
|||
# SPDX-FileCopyrightText: 2021 Jeff Epler
|
||||
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
12
.pylintrc
12
.pylintrc
|
|
@ -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
17
.readthedocs.yaml
Normal 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
|
||||
|
|
@ -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.
|
||||
41
Makefile
41
Makefile
|
|
@ -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
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -1,20 +1,16 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Jeff Epler
|
||||
SPDX-FileCopyrightText: 2021-2024 Jeff Epler
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
-->
|
||||
[](https://github.com/jepler/wwvbpy/actions/workflows/test.yml)
|
||||
[](https://codecov.io/gh/jepler/wwvbpy)
|
||||
[](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
|
||||
[](https://pypi.org/project/wwvb)
|
||||

|
||||
[](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml)
|
||||
[](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
|
||||
|
|
|
|||
|
|
@ -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]: ...
|
||||
0
_static/.empty → doc/_static/.empty
vendored
0
_static/.empty → doc/_static/.empty
vendored
|
|
@ -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
24
doc/index.rst
Normal 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:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
11
requirements.txt
Normal 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
|
||||
49
setup.cfg
49
setup.cfg
|
|
@ -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
|
||||
20
src/uwwvb.py
20
src/uwwvb.py
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
1
src/wwvb/iersdata.json
Normal 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"}
|
||||
2
src/wwvb/iersdata.json.license
Normal file
2
src/wwvb/iersdata.json.license
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Public domain
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -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),
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
# SPDX-FileCopyrightText: 2021 Jeff Epler
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
#
|
||||
# "For six minutes each half hour, from 10–16 and 40–46 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
|
||||
Loading…
Reference in a new issue