Compare commits
1 commit
main
...
pre-commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f36fa32a81 |
19 changed files with 97 additions and 161 deletions
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
|
|
@ -55,17 +55,10 @@ jobs:
|
|||
python -mpip install wheel
|
||||
python -mpip install -r requirements-dev.txt
|
||||
|
||||
- name: Check stubs with mypy
|
||||
- name: Check stubs
|
||||
if: (! startsWith(matrix.python-version, 'pypy-'))
|
||||
run: make mypy PYTHON=python
|
||||
|
||||
- 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:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ default_language_version:
|
|||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
|
|
@ -21,16 +21,10 @@ repos:
|
|||
- id: reuse
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.10
|
||||
rev: v0.12.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
- id: ruff
|
||||
args: [ --fix ]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.20.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [ --py39-plus ]
|
||||
exclude: src/uwwvb.py # CircuitPython prevaling standard!
|
||||
|
|
|
|||
13
Makefile
13
Makefile
|
|
@ -24,13 +24,13 @@ ENVPYTHON ?= _env/bin/python3
|
|||
endif
|
||||
|
||||
.PHONY: default
|
||||
default: coverage mypy pyright pyrefly
|
||||
default: coverage mypy
|
||||
|
||||
COVERAGE_INCLUDE=--include "src/**/*.py"
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(Q)$(PYTHON) -mcoverage erase
|
||||
$(Q)env PYTHONPATH=src $(PYTHON) -mcoverage run -p -m unittest discover -s test
|
||||
$(Q)env PYTHONPATH=src $(PYTHON) -mcoverage run --branch -p -m unittest discover -s test
|
||||
$(Q)$(PYTHON) -mcoverage combine -q
|
||||
$(Q)$(PYTHON) -mcoverage html $(COVERAGE_INCLUDE)
|
||||
$(Q)$(PYTHON) -mcoverage xml $(COVERAGE_INCLUDE)
|
||||
|
|
@ -46,15 +46,6 @@ test_venv:
|
|||
mypy:
|
||||
$(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:
|
||||
$(Q)env PYTHONPATH=src $(PYTHON) -mwwvb.updateiers --dist
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
|
||||
|
|
|
|||
0
codecov.yml
Normal file
0
codecov.yml
Normal file
|
|
@ -51,6 +51,5 @@ wwvbtk = "wwvb.wwvbtk:main"
|
|||
[[tool.mypy.overrides]]
|
||||
module = ["adafruit_datetime"]
|
||||
follow_untyped_imports = true
|
||||
[tool.coverage.run]
|
||||
patch=["subprocess"]
|
||||
branch=true
|
||||
[tool.coverage.report]
|
||||
exclude_also=["if TYPE_CHECKING:"]
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ adafruit-circuitpython-datetime
|
|||
beautifulsoup4
|
||||
build
|
||||
click
|
||||
coverage >= 7.10.3
|
||||
coverage >= 7.1.0
|
||||
mypy; implementation_name=="cpython"
|
||||
pyright; implementation_name=="cpython"
|
||||
pyrefly; implementation_name=="cpython"
|
||||
click>=8.1.5; implementation_name=="cpython"
|
||||
leapseconddata
|
||||
platformdirs
|
||||
|
|
|
|||
|
|
@ -19,36 +19,25 @@ import datetime
|
|||
import enum
|
||||
import json
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Literal
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, TextIO, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from . import iersdata
|
||||
from .tz import Mountain
|
||||
|
||||
WWVBChannel = Literal["amplitude", "phase", "both"]
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from typing import NotRequired, Self, TextIO, TypedDict, TypeVar
|
||||
|
||||
class JsonMinute(TypedDict):
|
||||
"""Implementation detail
|
||||
|
||||
This is the Python object type that is serialized by `print_timecodes_json`
|
||||
"""
|
||||
|
||||
year: int
|
||||
days: int
|
||||
hour: int
|
||||
minute: int
|
||||
amplitude: NotRequired[str]
|
||||
phase: NotRequired[str]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
HOUR = datetime.timedelta(seconds=3600)
|
||||
SECOND = datetime.timedelta(seconds=1)
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _removeprefix(s: str, p: str) -> str:
|
||||
if s.startswith(p):
|
||||
return s[len(p) :]
|
||||
return s
|
||||
|
||||
|
||||
def _date(dt: datetime.date) -> datetime.date:
|
||||
|
|
@ -352,13 +341,8 @@ class DstStatus(enum.IntEnum):
|
|||
"""DST in effect all day today"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WWVBMinute:
|
||||
"""Uniquely identifies a minute of time in the WWVB system.
|
||||
|
||||
To use ``ut1`` and ``ls`` information from IERS, create a `WWVBMinuteIERS`
|
||||
object instead.
|
||||
"""
|
||||
class _WWVBMinute(NamedTuple):
|
||||
"""(implementation detail)"""
|
||||
|
||||
year: int
|
||||
"""2-digit year within the WWVB epoch"""
|
||||
|
|
@ -369,7 +353,7 @@ class WWVBMinute:
|
|||
hour: int
|
||||
"""UTC hour of day"""
|
||||
|
||||
minute: int
|
||||
min: int
|
||||
"""Minute of hour"""
|
||||
|
||||
dst: DstStatus
|
||||
|
|
@ -384,10 +368,18 @@ class WWVBMinute:
|
|||
ly: bool
|
||||
"""Leap year flag"""
|
||||
|
||||
epoch: ClassVar[int] = 1970
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
class WWVBMinute(_WWVBMinute):
|
||||
"""Uniquely identifies a minute of time in the WWVB system.
|
||||
|
||||
To use ``ut1`` and ``ls`` information from IERS, create a `WWVBMinuteIERS`
|
||||
object instead.
|
||||
"""
|
||||
|
||||
epoch: int = 1970
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
year: int,
|
||||
days: int,
|
||||
hour: int,
|
||||
|
|
@ -397,7 +389,7 @@ class WWVBMinute:
|
|||
*,
|
||||
ls: bool | None = None,
|
||||
ly: bool | None = None,
|
||||
) -> None:
|
||||
) -> Self:
|
||||
"""Construct a WWVBMinute
|
||||
|
||||
:param year: The 2- or 4-digit year. This parameter is converted by the `full_year` method.
|
||||
|
|
@ -411,23 +403,15 @@ class WWVBMinute:
|
|||
:param ls: Leap second warning flag
|
||||
:param ly: Leap year flag
|
||||
"""
|
||||
dst = self.get_dst(year, days) if dst is None else DstStatus(dst)
|
||||
dst = cls.get_dst(year, days) if dst is None else DstStatus(dst)
|
||||
if ut1 is None and ls is None:
|
||||
ut1, ls = self._get_dut1_info(year, days)
|
||||
ut1, ls = cls._get_dut1_info(year, days)
|
||||
elif ut1 is None or ls is None:
|
||||
raise ValueError("sepecify both ut1 and ls or neither one")
|
||||
year = self.full_year(year)
|
||||
year = cls.full_year(year)
|
||||
if ly is None:
|
||||
ly = isly(year)
|
||||
|
||||
super().__setattr__("year", year)
|
||||
super().__setattr__("days", days)
|
||||
super().__setattr__("hour", hour)
|
||||
super().__setattr__("minute", minute)
|
||||
super().__setattr__("dst", dst)
|
||||
super().__setattr__("ut1", ut1)
|
||||
super().__setattr__("ls", ls)
|
||||
super().__setattr__("ly", ly)
|
||||
return _WWVBMinute.__new__(cls, year, days, hour, minute, dst, ut1, ls, ly)
|
||||
|
||||
@classmethod
|
||||
def full_year(cls, year: int) -> int:
|
||||
|
|
@ -461,7 +445,7 @@ class WWVBMinute:
|
|||
"""Implement str()"""
|
||||
return (
|
||||
f"year={self.year:4d} days={self.days:03d} hour={self.hour:02d} "
|
||||
f"min={self.minute:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} "
|
||||
f"min={self.min:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} "
|
||||
f"ls={int(self.ls)}"
|
||||
)
|
||||
|
||||
|
|
@ -471,7 +455,7 @@ class WWVBMinute:
|
|||
The returned object has ``tzinfo=datetime.timezone.utc``.
|
||||
"""
|
||||
d = datetime.datetime(self.year, 1, 1, tzinfo=datetime.timezone.utc)
|
||||
d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.minute * 60)
|
||||
d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.min * 60)
|
||||
return d
|
||||
|
||||
as_datetime = as_datetime_utc
|
||||
|
|
@ -525,7 +509,7 @@ class WWVBMinute:
|
|||
return 60
|
||||
if not self._is_end_of_month():
|
||||
return 60
|
||||
if self.hour != 23 or self.minute != 59:
|
||||
if self.hour != 23 or self.min != 59:
|
||||
return 60
|
||||
if self.ut1 > 0:
|
||||
return 59
|
||||
|
|
@ -569,7 +553,7 @@ class WWVBMinute:
|
|||
t.am[60] = AmplitudeModulation.MARK
|
||||
for i in [4, 10, 11, 14, 20, 21, 24, 34, 35, 44, 54]:
|
||||
t.am[i] = AmplitudeModulation.ZERO
|
||||
t._put_am_bcd(self.minute, 1, 2, 3, 5, 6, 7, 8)
|
||||
t._put_am_bcd(self.min, 1, 2, 3, 5, 6, 7, 8)
|
||||
t._put_am_bcd(self.hour, 12, 13, 15, 16, 17, 18)
|
||||
t._put_am_bcd(self.days, 22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
|
||||
ut1_sign = self.ut1 >= 0
|
||||
|
|
@ -583,14 +567,14 @@ class WWVBMinute:
|
|||
|
||||
def _fill_pm_timecode_extended(self, t: WWVBTimecode) -> None:
|
||||
"""During minutes 10..15 and 40..45, the amplitude signal holds 'extended information'"""
|
||||
assert 10 <= self.minute < 16 or 40 <= self.minute < 46
|
||||
minno = self.minute % 10
|
||||
assert 10 <= self.min < 16 or 40 <= self.min < 46
|
||||
minno = self.min % 10
|
||||
assert minno < 6
|
||||
|
||||
dst = self.dst
|
||||
# Note that these are 1 different than Table 11
|
||||
# because our LFSR sequence is zero-based
|
||||
seqno = (self.minute // 30) * 2
|
||||
seqno = (self.min // 30) * 2
|
||||
if dst == 0:
|
||||
pass
|
||||
elif dst == 3:
|
||||
|
|
@ -673,17 +657,17 @@ class WWVBMinute:
|
|||
|
||||
def _fill_pm_timecode(self, t: WWVBTimecode) -> None:
|
||||
"""Fill the phase portion of a timecode object"""
|
||||
if 10 <= self.minute < 16 or 40 <= self.minute < 46:
|
||||
if 10 <= self.min < 16 or 40 <= self.min < 46:
|
||||
self._fill_pm_timecode_extended(t)
|
||||
else:
|
||||
self._fill_pm_timecode_regular(t)
|
||||
|
||||
def next_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> Self:
|
||||
def next_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
||||
"""Return an object representing the next minute"""
|
||||
d = self.as_datetime() + datetime.timedelta(minutes=1)
|
||||
return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
|
||||
|
||||
def previous_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> Self:
|
||||
def previous_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
||||
"""Return an object representing the previous minute"""
|
||||
d = self.as_datetime() - datetime.timedelta(minutes=1)
|
||||
return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
|
||||
|
|
@ -702,9 +686,9 @@ class WWVBMinute:
|
|||
return 0, False
|
||||
|
||||
@classmethod
|
||||
def fromstring(cls, s: str) -> Self:
|
||||
def fromstring(cls, s: str) -> WWVBMinute:
|
||||
"""Construct a WWVBMinute from a string representation created by print_timecodes"""
|
||||
s = s.removeprefix("WWVB timecode: ")
|
||||
s = _removeprefix(s, "WWVB timecode: ")
|
||||
d: dict[str, int] = {}
|
||||
for part in s.split():
|
||||
k, v = part.split("=")
|
||||
|
|
@ -718,7 +702,7 @@ class WWVBMinute:
|
|||
dst = d.pop("dst", None)
|
||||
ut1 = d.pop("ut1", None)
|
||||
ls = d.pop("ls", None)
|
||||
d.pop("ly", None) # Always use calculated ly flag
|
||||
d.pop("ly", None)
|
||||
if d:
|
||||
raise ValueError(f"Invalid options: {d}")
|
||||
return cls(year, days, hour, minute, dst, ut1=ut1, ls=None if ls is None else bool(ls))
|
||||
|
|
@ -731,7 +715,7 @@ class WWVBMinute:
|
|||
newut1: int | None = None,
|
||||
newls: bool | None = None,
|
||||
old_time: WWVBMinute | None = None,
|
||||
) -> Self:
|
||||
) -> WWVBMinute:
|
||||
"""Construct a WWVBMinute from a datetime, possibly specifying ut1/ls data or propagating it from an old time"""
|
||||
u = d.utctimetuple()
|
||||
if newls is None and newut1 is None:
|
||||
|
|
@ -739,7 +723,7 @@ class WWVBMinute:
|
|||
return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
|
||||
|
||||
@classmethod
|
||||
def from_timecode_am(cls, t: WWVBTimecode) -> Self | None: # noqa: PLR0912
|
||||
def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None: # noqa: PLR0912
|
||||
"""Construct a WWVBMinute from a WWVBTimecode"""
|
||||
for i in (0, 9, 19, 29, 39, 49, 59):
|
||||
if t.am[i] != AmplitudeModulation.MARK:
|
||||
|
|
@ -782,15 +766,6 @@ class WWVBMinute:
|
|||
return None
|
||||
return cls(year, days, hour, minute, dst, ut1, ls=ls, ly=ly)
|
||||
|
||||
@property
|
||||
def min(self) -> int:
|
||||
"""Deprecated alias for `WWVBMinute.minute`
|
||||
|
||||
Update your code to use the `minute` property instead of the `min` property.
|
||||
"""
|
||||
warnings.warn("WWVBMinute.min property is deprecated", category=DeprecationWarning, stacklevel=1)
|
||||
return self.minute
|
||||
|
||||
|
||||
class WWVBMinuteIERS(WWVBMinute):
|
||||
"""A WWVBMinute that uses a database of DUT1 information"""
|
||||
|
|
@ -942,7 +917,7 @@ styles = {
|
|||
def print_timecodes(
|
||||
w: WWVBMinute,
|
||||
minutes: int,
|
||||
channel: WWVBChannel,
|
||||
channel: str,
|
||||
style: str,
|
||||
file: TextIO,
|
||||
*,
|
||||
|
|
@ -961,7 +936,7 @@ def print_timecodes(
|
|||
print(file=file)
|
||||
print(f"WWVB timecode: {w!s}{channel_text}{style_text}", file=file)
|
||||
first = False
|
||||
pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.minute:02d} "
|
||||
pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.min:02d} "
|
||||
tc = w.as_timecode()
|
||||
if len(style_chars) == 6:
|
||||
print(f"{pfx} {tc.to_both_string(style_chars)}", file=file)
|
||||
|
|
@ -979,7 +954,7 @@ def print_timecodes(
|
|||
def print_timecodes_json(
|
||||
w: WWVBMinute,
|
||||
minutes: int,
|
||||
channel: WWVBChannel,
|
||||
channel: str,
|
||||
file: TextIO,
|
||||
) -> None:
|
||||
"""Print a range of timecodes in JSON format.
|
||||
|
|
@ -999,11 +974,11 @@ def print_timecodes_json(
|
|||
"""
|
||||
result = []
|
||||
for _ in range(minutes):
|
||||
data: JsonMinute = {
|
||||
data: dict[str, Any] = {
|
||||
"year": w.year,
|
||||
"days": w.days,
|
||||
"hour": w.hour,
|
||||
"minute": w.minute,
|
||||
"minute": w.min,
|
||||
}
|
||||
|
||||
tc = w.as_timecode()
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import wwvb
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
|
|
|
|||
|
|
@ -9,18 +9,15 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
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(ctx: click.Context, param: click.Parameter, value: list[str]) -> datetime.datetime: # noqa: ARG001
|
||||
def parse_timespec(ctx: Any, param: Any, value: list[str]) -> datetime.datetime: # noqa: ARG001
|
||||
"""Parse a time specifier from the commandline"""
|
||||
try:
|
||||
if len(value) == 5:
|
||||
|
|
@ -98,7 +95,7 @@ def main(
|
|||
dut1: int,
|
||||
minutes: int,
|
||||
style: str,
|
||||
channel: WWVBChannel,
|
||||
channel: str,
|
||||
all_timecodes: bool,
|
||||
timespec: datetime.datetime,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -1 +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"}
|
||||
{"START": "1972-01-01", "OFFSETS_GZ": "H4sIAIYEZWgC/+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/ubKM4cgwMnHZW4GFxFprOVcevAKGva6oExn1MOmyGDJQPm0rpU8bjqdOo993O6Xz9ofToZela5vwrWoTn4l4o5CIIaKejCEgSnJv784V6y0/IJeROtycVNAAA="}
|
||||
|
|
@ -47,7 +47,6 @@ def update_iersdata( # noqa: PLR0915
|
|||
"""Update iersdata.py"""
|
||||
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"]
|
||||
|
|
@ -80,8 +79,6 @@ def update_iersdata( # noqa: PLR0915
|
|||
|
||||
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]
|
||||
|
|
@ -91,7 +88,6 @@ def update_iersdata( # noqa: PLR0915
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
import functools
|
||||
from tkinter import Canvas, Event, TclError, Tk
|
||||
from tkinter import Canvas, TclError, Tk
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import click
|
||||
|
||||
import wwvb
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ def _app() -> Tk:
|
|||
return Tk()
|
||||
|
||||
|
||||
def validate_colors(ctx: click.Context, param: click.Parameter, value: str) -> list[str]: # noqa: ARG001
|
||||
def validate_colors(ctx: Any, param: Any, 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()
|
||||
|
|
@ -69,7 +69,7 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
|
|||
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)
|
||||
return int(max(0, (deadline - now).total_seconds()) * 1000)
|
||||
|
||||
def wwvbtick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
|
||||
"""Yield consecutive values of the WWVB amplitude signal, going from minute to minute"""
|
||||
|
|
@ -106,7 +106,7 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
|
|||
canvas.pack(fill="both", expand=True)
|
||||
app.wm_deiconify()
|
||||
|
||||
def resize_canvas(event: Event) -> None:
|
||||
def resize_canvas(event: Any) -> None:
|
||||
"""Keep the circle filling the window when it is resized"""
|
||||
sz = min(event.width, event.height) - 8
|
||||
if sz < 0:
|
||||
|
|
@ -141,12 +141,10 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
|
|||
|
||||
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,5 +1,4 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
"""Test most wwvblib commandline programs"""
|
||||
|
||||
# ruff: noqa: N802 D102
|
||||
|
|
@ -7,13 +6,13 @@
|
|||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
# These imports must remain, even though the module contents are not used directly!
|
||||
import wwvb.dut1table
|
||||
|
|
@ -23,11 +22,7 @@ import wwvb.gen
|
|||
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
|
||||
coverage_add = ("-m", "coverage", "run", "--branch", "-p") if "COVERAGE_RUN" in os.environ else ()
|
||||
|
||||
|
||||
class CLITestCase(unittest.TestCase):
|
||||
|
|
@ -39,10 +34,10 @@ class CLITestCase(unittest.TestCase):
|
|||
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)
|
||||
return (sys.executable, *coverage_add, "-m", *args)
|
||||
|
||||
def moduleOutput(self, *args: str) -> str:
|
||||
return self.programOutput(sys.executable, "-m", *args)
|
||||
return self.programOutput(sys.executable, *coverage_add, "-m", *args)
|
||||
|
||||
def assertProgramOutput(self, expected: str, *args: str) -> None:
|
||||
"""Check the output from invoking a program matches the expected"""
|
||||
|
|
@ -62,10 +57,9 @@ class CLITestCase(unittest.TestCase):
|
|||
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:
|
||||
def assertModuleJson(self, expected: Any, *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:
|
||||
|
|
|
|||
|
|
@ -56,3 +56,7 @@ class TestLeapSecond(unittest.TestCase):
|
|||
assert not our_is_ls
|
||||
d = datetime.datetime.combine(nm, datetime.time(), tzinfo=datetime.timezone.utc)
|
||||
self.assertEqual(leap, bench)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -27,3 +27,7 @@ class TestPhaseModulation(unittest.TestCase):
|
|||
|
||||
self.assertEqual(ref_am, test_am)
|
||||
self.assertEqual(ref_pm, test_pm)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|||
any_leap_second = False
|
||||
for _ in range(20):
|
||||
timecode = minute.as_timecode()
|
||||
decoded: uwwvb.WWVBMinute | None = None
|
||||
decoded = None
|
||||
if len(timecode.am) == 61:
|
||||
any_leap_second = True
|
||||
for code in timecode.am:
|
||||
|
|
@ -215,3 +215,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|||
datetime.datetime(2020, 12, 31, 17, 00, tzinfo=zoneinfo.ZoneInfo("America/Denver")), # Mountain time!
|
||||
uwwvb.as_datetime_local(decoded),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import unittest
|
|||
|
||||
import uwwvb
|
||||
import wwvb
|
||||
from wwvb import WWVBChannel, decode, iersdata, tz
|
||||
from wwvb import decode, iersdata, tz
|
||||
|
||||
|
||||
class WWVBMinute2k(wwvb.WWVBMinute):
|
||||
|
|
@ -44,15 +44,11 @@ class WWVBTestCase(unittest.TestCase):
|
|||
header = lines[0].split()
|
||||
timestamp = " ".join(header[:10])
|
||||
options = header[10:]
|
||||
channel: WWVBChannel = "amplitude"
|
||||
channel = "amplitude"
|
||||
style = "default"
|
||||
for o in options:
|
||||
if o == "--channel=both":
|
||||
channel = "both"
|
||||
elif o == "--channel=amplitude":
|
||||
channel = "amplitude"
|
||||
elif o == "--channel=phase":
|
||||
channel = "phase"
|
||||
if o.startswith("--channel="):
|
||||
channel = o[10:]
|
||||
elif o.startswith("--style="):
|
||||
style = o[8:]
|
||||
else:
|
||||
|
|
@ -310,11 +306,6 @@ class WWVBRoundtrip(unittest.TestCase):
|
|||
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"""
|
||||
with self.assertWarnsRegex(Warning, "is unset"):
|
||||
|
|
@ -434,3 +425,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|||
minute.am[57] = wwvb.AmplitudeModulation.MARK
|
||||
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
|
||||
assert decoded_minute is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
# 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