wwvb 3.0.6__py3-none-any.whl → 3.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uwwvb.py +5 -5
- wwvb/__init__.py +35 -49
- wwvb/__version__.py +2 -2
- wwvb/decode.py +2 -7
- wwvb/dut1table.py +1 -0
- wwvb/gen.py +4 -13
- wwvb/iersdata.py +2 -4
- wwvb/iersdata_dist.py +2 -3
- wwvb/testcli.py +87 -41
- wwvb/testdaylight.py +4 -12
- wwvb/testls.py +1 -3
- wwvb/testpm.py +3 -17
- wwvb/testuwwvb.py +11 -25
- wwvb/testwwvb.py +17 -52
- wwvb/updateiers.py +8 -20
- wwvb/wwvbtk.py +3 -7
- {wwvb-3.0.6.dist-info → wwvb-3.0.8.dist-info}/METADATA +1 -1
- wwvb-3.0.8.dist-info/RECORD +23 -0
- {wwvb-3.0.6.dist-info → wwvb-3.0.8.dist-info}/WHEEL +1 -1
- wwvb-3.0.6.dist-info/RECORD +0 -23
- {wwvb-3.0.6.dist-info → wwvb-3.0.8.dist-info}/entry_points.txt +0 -0
- {wwvb-3.0.6.dist-info → wwvb-3.0.8.dist-info}/top_level.txt +0 -0
uwwvb.py
CHANGED
@@ -16,9 +16,7 @@ always_mark = set((0, 9, 19, 29, 39, 49, 59))
|
|
16
16
|
always_zero = set((4, 10, 11, 14, 20, 21, 34, 35, 44, 54))
|
17
17
|
bcd_weights = (1, 2, 4, 8, 10, 20, 40, 80, 100, 200, 400, 800)
|
18
18
|
|
19
|
-
WWVBMinute = namedtuple(
|
20
|
-
"WWVBMinute", ["year", "days", "hour", "minute", "dst", "ut1", "ls", "ly"]
|
21
|
-
)
|
19
|
+
WWVBMinute = namedtuple("WWVBMinute", ["year", "days", "hour", "minute", "dst", "ut1", "ls", "ly"])
|
22
20
|
|
23
21
|
|
24
22
|
class WWVBDecoder:
|
@@ -30,7 +28,9 @@ class WWVBDecoder:
|
|
30
28
|
self.state = 1
|
31
29
|
|
32
30
|
def update(self, value: int) -> list[int] | None:
|
33
|
-
"""Update the _state machine when a new symbol is received.
|
31
|
+
"""Update the _state machine when a new symbol is received.
|
32
|
+
|
33
|
+
If a possible complete _minute is received, return it; otherwise, return None"""
|
34
34
|
result = None
|
35
35
|
if self.state == 1:
|
36
36
|
self.minute = []
|
@@ -87,7 +87,7 @@ def get_am_bcd(seq: list[int], *poslist: int) -> int | None:
|
|
87
87
|
return result
|
88
88
|
|
89
89
|
|
90
|
-
def decode_wwvb(
|
90
|
+
def decode_wwvb(
|
91
91
|
t: list[int] | None,
|
92
92
|
) -> WWVBMinute | None:
|
93
93
|
"""Convert a received minute of wwvb symbols to a WWVBMinute. Returns None if any error is detected."""
|
wwvb/__init__.py
CHANGED
@@ -19,7 +19,7 @@ from .tz import Mountain
|
|
19
19
|
HOUR = datetime.timedelta(seconds=3600)
|
20
20
|
SECOND = datetime.timedelta(seconds=1)
|
21
21
|
DateOrDatetime = TypeVar("DateOrDatetime", datetime.date, datetime.datetime)
|
22
|
-
T = TypeVar("T")
|
22
|
+
T = TypeVar("T")
|
23
23
|
|
24
24
|
|
25
25
|
def require(x: Optional[T]) -> T:
|
@@ -49,9 +49,7 @@ def _maybe_warn_update(dt: datetime.date) -> None:
|
|
49
49
|
# prospective available now.
|
50
50
|
today = datetime.date.today()
|
51
51
|
if _date(dt) < today + datetime.timedelta(days=330):
|
52
|
-
warnings.warn(
|
53
|
-
"Note: Running `updateiers` may provide better DUT1 and LS information"
|
54
|
-
)
|
52
|
+
warnings.warn("Note: Running `updateiers` may provide better DUT1 and LS information")
|
55
53
|
|
56
54
|
|
57
55
|
def get_dut1(dt: DateOrDatetime, *, warn_outdated: bool = True) -> float:
|
@@ -110,9 +108,7 @@ def is_dst_change_day(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
|
|
110
108
|
return isdst(t, tz) != isdst(t + datetime.timedelta(1), tz)
|
111
109
|
|
112
110
|
|
113
|
-
def get_dst_change_hour(
|
114
|
-
t: DateOrDatetime, tz: datetime.tzinfo = Mountain
|
115
|
-
) -> Optional[int]:
|
111
|
+
def get_dst_change_hour(t: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> Optional[int]:
|
116
112
|
"""Return the hour when DST changes"""
|
117
113
|
lt0 = datetime.datetime(t.year, t.month, t.day, hour=0, tzinfo=tz)
|
118
114
|
dst0 = lt0.dst()
|
@@ -291,7 +287,9 @@ def extract_bit(v: int, p: int) -> bool:
|
|
291
287
|
|
292
288
|
|
293
289
|
def hamming_parity(value: int) -> int:
|
294
|
-
"""Compute the "hamming parity" of a 26-bit number, such as the minute-of-century
|
290
|
+
"""Compute the "hamming parity" of a 26-bit number, such as the minute-of-century
|
291
|
+
|
292
|
+
For more details, see Enhanced WWVB Broadcast Format 4.3"""
|
295
293
|
parity = 0
|
296
294
|
for i in range(4, -1, -1):
|
297
295
|
bit = 0
|
@@ -324,7 +322,9 @@ _WWVBMinute = collections.namedtuple("_WWVBMinute", "year days hour min dst ut1
|
|
324
322
|
|
325
323
|
|
326
324
|
class WWVBMinute(_WWVBMinute):
|
327
|
-
"""Uniquely identifies a minute of time in the WWVB system.
|
325
|
+
"""Uniquely identifies a minute of time in the WWVB system.
|
326
|
+
|
327
|
+
To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead."""
|
328
328
|
|
329
329
|
year: int
|
330
330
|
hour: int
|
@@ -336,7 +336,7 @@ class WWVBMinute(_WWVBMinute):
|
|
336
336
|
|
337
337
|
epoch: int = 1970
|
338
338
|
|
339
|
-
def __new__(
|
339
|
+
def __new__(
|
340
340
|
cls,
|
341
341
|
year: int,
|
342
342
|
days: int,
|
@@ -391,7 +391,11 @@ class WWVBMinute(_WWVBMinute):
|
|
391
391
|
|
392
392
|
def __str__(self) -> str:
|
393
393
|
"""Implement str()"""
|
394
|
-
return
|
394
|
+
return (
|
395
|
+
f"year={self.year:4d} days={self.days:03d} hour={self.hour:02d} "
|
396
|
+
f"min={self.min:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} "
|
397
|
+
f"ls={int(self.ls)}"
|
398
|
+
)
|
395
399
|
|
396
400
|
def as_datetime_utc(self) -> datetime.datetime:
|
397
401
|
"""Convert to a UTC datetime"""
|
@@ -401,9 +405,7 @@ class WWVBMinute(_WWVBMinute):
|
|
401
405
|
|
402
406
|
as_datetime = as_datetime_utc
|
403
407
|
|
404
|
-
def as_datetime_local(
|
405
|
-
self, standard_time_offset: int = 7 * 3600, dst_observed: bool = True
|
406
|
-
) -> datetime.datetime:
|
408
|
+
def as_datetime_local(self, standard_time_offset: int = 7 * 3600, dst_observed: bool = True) -> datetime.datetime:
|
407
409
|
"""Convert to a local datetime according to the DST bits"""
|
408
410
|
u = self.as_datetime_utc()
|
409
411
|
offset = datetime.timedelta(seconds=-standard_time_offset)
|
@@ -471,12 +473,7 @@ class WWVBMinute(_WWVBMinute):
|
|
471
473
|
century = (self.year // 100) * 100
|
472
474
|
# note: This relies on timedelta seconds never including leapseconds!
|
473
475
|
return (
|
474
|
-
int(
|
475
|
-
(
|
476
|
-
self.as_datetime()
|
477
|
-
- datetime.datetime(century, 1, 1, tzinfo=datetime.timezone.utc)
|
478
|
-
).total_seconds()
|
479
|
-
)
|
476
|
+
int((self.as_datetime() - datetime.datetime(century, 1, 1, tzinfo=datetime.timezone.utc)).total_seconds())
|
480
477
|
// 60
|
481
478
|
)
|
482
479
|
|
@@ -497,9 +494,7 @@ class WWVBMinute(_WWVBMinute):
|
|
497
494
|
t.am[36] = t.am[38] = AmplitudeModulation(ut1_sign)
|
498
495
|
t.am[37] = AmplitudeModulation(not ut1_sign)
|
499
496
|
t._put_am_bcd(abs(self.ut1) // 100, 40, 41, 42, 43)
|
500
|
-
t._put_am_bcd(
|
501
|
-
self.year, 45, 46, 47, 48, 50, 51, 52, 53
|
502
|
-
) # Implicitly discards all but lowest 2 digits of year
|
497
|
+
t._put_am_bcd(self.year, 45, 46, 47, 48, 50, 51, 52, 53) # Implicitly discards all but lowest 2 digits of year
|
503
498
|
t.am[55] = AmplitudeModulation(self.ly)
|
504
499
|
t.am[56] = AmplitudeModulation(self.ls)
|
505
500
|
t._put_am_bcd(self.dst, 57, 58)
|
@@ -539,9 +534,7 @@ class WWVBMinute(_WWVBMinute):
|
|
539
534
|
for i in range(60):
|
540
535
|
t._put_pm_bit(i, full_seq[i + offset])
|
541
536
|
|
542
|
-
def fill_pm_timecode_regular(
|
543
|
-
self, t: "WWVBTimecode"
|
544
|
-
) -> None:
|
537
|
+
def fill_pm_timecode_regular(self, t: "WWVBTimecode") -> None:
|
545
538
|
"""Except during minutes 10..15 and 40..45, the amplitude signal holds 'regular information'"""
|
546
539
|
t._put_pm_bin(0, 13, SYNC_T)
|
547
540
|
|
@@ -604,24 +597,18 @@ class WWVBMinute(_WWVBMinute):
|
|
604
597
|
else:
|
605
598
|
self.fill_pm_timecode_regular(t)
|
606
599
|
|
607
|
-
def next_minute(
|
608
|
-
self, newut1: Optional[int] = None, newls: Optional[bool] = None
|
609
|
-
) -> "WWVBMinute":
|
600
|
+
def next_minute(self, newut1: Optional[int] = None, newls: Optional[bool] = None) -> "WWVBMinute":
|
610
601
|
"""Return an object representing the next minute"""
|
611
602
|
d = self.as_datetime() + datetime.timedelta(minutes=1)
|
612
603
|
return self.from_datetime(d, newut1, newls, self)
|
613
604
|
|
614
|
-
def previous_minute(
|
615
|
-
self, newut1: Optional[int] = None, newls: Optional[bool] = None
|
616
|
-
) -> "WWVBMinute":
|
605
|
+
def previous_minute(self, newut1: Optional[int] = None, newls: Optional[bool] = None) -> "WWVBMinute":
|
617
606
|
"""Return an object representing the previous minute"""
|
618
607
|
d = self.as_datetime() - datetime.timedelta(minutes=1)
|
619
608
|
return self.from_datetime(d, newut1, newls, self)
|
620
609
|
|
621
610
|
@classmethod
|
622
|
-
def _get_dut1_info(
|
623
|
-
cls: type, year: int, days: int, old_time: "Optional[WWVBMinute]" = None
|
624
|
-
) -> Tuple[int, bool]:
|
611
|
+
def _get_dut1_info(cls: type, year: int, days: int, old_time: "Optional[WWVBMinute]" = None) -> Tuple[int, bool]:
|
625
612
|
"""Return the DUT1 information for a given day, possibly propagating information from a previous timestamp"""
|
626
613
|
if old_time is not None:
|
627
614
|
if old_time.minute_length() != 60:
|
@@ -673,9 +660,7 @@ class WWVBMinute(_WWVBMinute):
|
|
673
660
|
return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
|
674
661
|
|
675
662
|
@classmethod
|
676
|
-
def from_timecode_am(
|
677
|
-
cls, t: "WWVBTimecode"
|
678
|
-
) -> Optional["WWVBMinute"]:
|
663
|
+
def from_timecode_am(cls, t: "WWVBTimecode") -> Optional["WWVBMinute"]:
|
679
664
|
"""Construct a WWVBMinute from a WWVBTimecode"""
|
680
665
|
for i in (0, 9, 19, 29, 39, 49, 59):
|
681
666
|
if t.am[i] != AmplitudeModulation.MARK:
|
@@ -717,9 +702,7 @@ class WWVBMinuteIERS(WWVBMinute):
|
|
717
702
|
"""A WWVBMinute that uses a database of DUT1 information"""
|
718
703
|
|
719
704
|
@classmethod
|
720
|
-
def _get_dut1_info(
|
721
|
-
cls, year: int, days: int, old_time: Optional[WWVBMinute] = None
|
722
|
-
) -> Tuple[int, bool]:
|
705
|
+
def _get_dut1_info(cls, year: int, days: int, old_time: Optional[WWVBMinute] = None) -> Tuple[int, bool]:
|
723
706
|
d = datetime.datetime(year, 1, 1) + datetime.timedelta(days - 1)
|
724
707
|
return int(round(get_dut1(d) * 10)) * 100, isls(d)
|
725
708
|
|
@@ -759,11 +742,14 @@ class WWVBTimecode:
|
|
759
742
|
phase: List[PhaseModulation]
|
760
743
|
|
761
744
|
def __init__(self, sz: int) -> None:
|
762
|
-
self.am = [AmplitudeModulation.UNSET] * sz
|
745
|
+
self.am = [AmplitudeModulation.UNSET] * sz
|
763
746
|
self.phase = [PhaseModulation.UNSET] * sz
|
764
747
|
|
765
748
|
def _get_am_bcd(self, *poslist: int) -> Optional[int]:
|
766
|
-
"""Convert
|
749
|
+
"""Convert AM data to BCD
|
750
|
+
|
751
|
+
The the bits ``self.am[poslist[i]]`` in MSB order are converted from
|
752
|
+
BCD to integer"""
|
767
753
|
pos = reversed(poslist)
|
768
754
|
val = [bool(self.am[p]) for p in pos]
|
769
755
|
result = 0
|
@@ -779,7 +765,11 @@ class WWVBTimecode:
|
|
779
765
|
return result
|
780
766
|
|
781
767
|
def _put_am_bcd(self, v: int, *poslist: int) -> None:
|
782
|
-
"""
|
768
|
+
"""Insert BCD coded data into the AM signal
|
769
|
+
|
770
|
+
The bits at ``self.am[poslist[i]]`` in MSB order are filled with
|
771
|
+
the conversion of `v` to BCD
|
772
|
+
Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number"""
|
783
773
|
pos = list(poslist)[::-1]
|
784
774
|
for p, b in zip(pos, bcd_bits(v)):
|
785
775
|
if b:
|
@@ -798,9 +788,7 @@ class WWVBTimecode:
|
|
798
788
|
|
799
789
|
def __str__(self) -> str:
|
800
790
|
"""implement str()"""
|
801
|
-
undefined = [
|
802
|
-
i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET
|
803
|
-
]
|
791
|
+
undefined = [i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET]
|
804
792
|
if undefined:
|
805
793
|
warnings.warn(f"am{undefined} is unset")
|
806
794
|
|
@@ -841,7 +829,6 @@ styles = {
|
|
841
829
|
}
|
842
830
|
|
843
831
|
|
844
|
-
# pylint: disable=too-many-arguments
|
845
832
|
def print_timecodes(
|
846
833
|
w: WWVBMinute,
|
847
834
|
minutes: int,
|
@@ -879,7 +866,6 @@ def print_timecodes(
|
|
879
866
|
w = w.next_minute()
|
880
867
|
|
881
868
|
|
882
|
-
# pylint: disable=too-many-arguments
|
883
869
|
def print_timecodes_json(
|
884
870
|
w: WWVBMinute,
|
885
871
|
minutes: int,
|
wwvb/__version__.py
CHANGED
wwvb/decode.py
CHANGED
@@ -23,9 +23,7 @@ import wwvb
|
|
23
23
|
always_zero = set((4, 10, 11, 14, 20, 21, 34, 35, 44, 54))
|
24
24
|
|
25
25
|
|
26
|
-
def wwvbreceive() ->
|
27
|
-
Generator[Optional[wwvb.WWVBTimecode], wwvb.AmplitudeModulation, None]
|
28
|
-
): # pylint: disable=too-many-branches
|
26
|
+
def wwvbreceive() -> Generator[Optional[wwvb.WWVBTimecode], wwvb.AmplitudeModulation, None]:
|
29
27
|
"""A stateful decoder of WWVB signals"""
|
30
28
|
minute: List[wwvb.AmplitudeModulation] = []
|
31
29
|
state = 1
|
@@ -60,10 +58,7 @@ def wwvbreceive() -> (
|
|
60
58
|
elif len(minute) % 10 and value == wwvb.AmplitudeModulation.MARK:
|
61
59
|
# print("UNEXPECTED MARK")
|
62
60
|
state = 1
|
63
|
-
elif (
|
64
|
-
len(minute) - 1 in always_zero
|
65
|
-
and value != wwvb.AmplitudeModulation.ZERO
|
66
|
-
):
|
61
|
+
elif len(minute) - 1 in always_zero and value != wwvb.AmplitudeModulation.ZERO:
|
67
62
|
# print("UNEXPECTED NONZERO")
|
68
63
|
state = 1
|
69
64
|
elif len(minute) == 60:
|
wwvb/dut1table.py
CHANGED
wwvb/gen.py
CHANGED
@@ -16,9 +16,7 @@ import dateutil.parser
|
|
16
16
|
from . import WWVBMinute, WWVBMinuteIERS, print_timecodes, print_timecodes_json, styles
|
17
17
|
|
18
18
|
|
19
|
-
def parse_timespec(
|
20
|
-
ctx: Any, param: Any, value: List[str]
|
21
|
-
) -> datetime.datetime:
|
19
|
+
def parse_timespec(ctx: Any, param: Any, value: List[str]) -> datetime.datetime:
|
22
20
|
"""Parse a time specifier from the commandline"""
|
23
21
|
try:
|
24
22
|
if len(value) == 5:
|
@@ -26,9 +24,7 @@ def parse_timespec( # pylint: disable=unused-argument
|
|
26
24
|
return datetime.datetime(year, month, day, hour, minute)
|
27
25
|
if len(value) == 4:
|
28
26
|
year, yday, hour, minute = map(int, value)
|
29
|
-
return datetime.datetime(year, 1, 1, hour, minute) + datetime.timedelta(
|
30
|
-
days=yday - 1
|
31
|
-
)
|
27
|
+
return datetime.datetime(year, 1, 1, hour, minute) + datetime.timedelta(days=yday - 1)
|
32
28
|
if len(value) == 1:
|
33
29
|
return dateutil.parser.parse(value[0])
|
34
30
|
if len(value) == 0:
|
@@ -68,9 +64,7 @@ def parse_timespec( # pylint: disable=unused-argument
|
|
68
64
|
help="Force no leap second at the end of the month (Implies --no-iers)",
|
69
65
|
)
|
70
66
|
@click.option("--dut1", "-d", type=int, help="Force the DUT1 value (Implies --no-iers)")
|
71
|
-
@click.option(
|
72
|
-
"--minutes", "-m", default=10, help="Number of minutes to show (default: 10)"
|
73
|
-
)
|
67
|
+
@click.option("--minutes", "-m", default=10, help="Number of minutes to show (default: 10)")
|
74
68
|
@click.option(
|
75
69
|
"--style",
|
76
70
|
default="default",
|
@@ -91,7 +85,6 @@ def parse_timespec( # pylint: disable=unused-argument
|
|
91
85
|
help="Modulation to show (default: amplitude)",
|
92
86
|
)
|
93
87
|
@click.argument("timespec", type=str, nargs=-1, callback=parse_timespec)
|
94
|
-
# pylint: disable=too-many-arguments, too-many-locals
|
95
88
|
def main(
|
96
89
|
iers: bool,
|
97
90
|
leap_second: bool,
|
@@ -127,9 +120,7 @@ def main(
|
|
127
120
|
if style == "json":
|
128
121
|
print_timecodes_json(w, minutes, channel, file=sys.stdout)
|
129
122
|
else:
|
130
|
-
print_timecodes(
|
131
|
-
w, minutes, channel, style, all_timecodes=all_timecodes, file=sys.stdout
|
132
|
-
)
|
123
|
+
print_timecodes(w, minutes, channel, style, all_timecodes=all_timecodes, file=sys.stdout)
|
133
124
|
|
134
125
|
|
135
126
|
if __name__ == "__main__": # pragma no branch
|
wwvb/iersdata.py
CHANGED
@@ -21,11 +21,9 @@ for location in [
|
|
21
21
|
filename = os.path.join(location, "wwvbpy_iersdata.py")
|
22
22
|
if os.path.exists(filename):
|
23
23
|
with open(filename, encoding="utf-8") as f:
|
24
|
-
exec(f.read(), globals(), globals())
|
24
|
+
exec(f.read(), globals(), globals())
|
25
25
|
break
|
26
26
|
|
27
|
-
start = datetime.datetime.combine(DUT1_DATA_START, datetime.time()).replace(
|
28
|
-
tzinfo=datetime.timezone.utc
|
29
|
-
)
|
27
|
+
start = datetime.datetime.combine(DUT1_DATA_START, datetime.time()).replace(tzinfo=datetime.timezone.utc)
|
30
28
|
span = datetime.timedelta(days=len(DUT1_OFFSETS))
|
31
29
|
end = start + span
|
wwvb/iersdata_dist.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
# -*- python3 -*-
|
2
|
+
# fmt: off
|
2
3
|
"""File generated from public data - not subject to copyright"""
|
3
4
|
# SPDX-FileCopyrightText: Public domain
|
4
5
|
# SPDX-License-Identifier: CC0-1.0
|
5
|
-
# fmt: off
|
6
6
|
# isort: skip_file
|
7
|
-
# pylint: disable=invalid-name
|
8
7
|
import datetime
|
9
8
|
__all__ = ['DUT1_DATA_START', 'DUT1_OFFSETS']
|
10
9
|
DUT1_DATA_START = datetime.date(1972, 1, 1)
|
@@ -35,5 +34,5 @@ DUT1_OFFSETS = str( # 19720101
|
|
35
34
|
+i*126+h*176+g*97+f*91+e*52+o*116+n*98+m*70+l*133+k*91+j*91 # 20140507
|
36
35
|
+i*77+h*140+g*91+f*84+e*70+d*34+n*72+m*76+l*66+k*53+j*56 # 20160831
|
37
36
|
+i*105+h*77+g*45+q*25+p*63+o*91+n*154+m*105+l*190+k*118 # 20190501
|
38
|
-
+j*105+i*807+j*376+k*
|
37
|
+
+j*105+i*807+j*376+k*775+l*67+k*2+l*6+k*133 # 20250405
|
39
38
|
)
|
wwvb/testcli.py
CHANGED
@@ -6,50 +6,57 @@
|
|
6
6
|
#
|
7
7
|
# SPDX-License-Identifier: GPL-3.0-only
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
import json
|
11
10
|
import os
|
12
11
|
import subprocess
|
13
12
|
import sys
|
14
13
|
import unittest
|
14
|
+
from typing import Any, Sequence
|
15
15
|
|
16
|
-
coverage_add = (
|
17
|
-
("-m", "coverage", "run", "--branch", "-p") if "COVERAGE_RUN" in os.environ else ()
|
18
|
-
)
|
16
|
+
coverage_add = ("-m", "coverage", "run", "--branch", "-p") if "COVERAGE_RUN" in os.environ else ()
|
19
17
|
|
20
18
|
|
21
19
|
class CLITestCase(unittest.TestCase):
|
22
20
|
"""Test various CLI commands within wwvbpy"""
|
23
21
|
|
24
|
-
def
|
25
|
-
"""Check the output from invoking a program matches the expected"""
|
22
|
+
def programOutput(self, *args: str) -> str:
|
26
23
|
env = os.environ.copy()
|
27
24
|
env["PYTHONIOENCODING"] = "utf-8"
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
return subprocess.check_output(args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env)
|
26
|
+
|
27
|
+
def moduleArgs(self, *args: str) -> Sequence[str]:
|
28
|
+
return tuple((sys.executable, *coverage_add, "-m", *args))
|
29
|
+
|
30
|
+
def moduleOutput(self, *args: str) -> str:
|
31
|
+
return self.programOutput(sys.executable, *coverage_add, "-m", *args)
|
32
|
+
|
33
|
+
def assertProgramOutput(self, expected: str, *args: str) -> None:
|
34
|
+
"""Check the output from invoking a program matches the expected"""
|
35
|
+
actual = self.programOutput(*args)
|
31
36
|
self.assertMultiLineEqual(expected, actual, f"args={args}")
|
32
37
|
|
33
38
|
def assertProgramOutputStarts(self, expected: str, *args: str) -> None:
|
34
39
|
"""Check the output from invoking a program matches the expected"""
|
35
|
-
|
36
|
-
env["PYTHONIOENCODING"] = "utf-8"
|
37
|
-
actual = subprocess.check_output(
|
38
|
-
args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env
|
39
|
-
)
|
40
|
+
actual = self.programOutput(*args)
|
40
41
|
self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}")
|
41
42
|
|
42
43
|
def assertModuleOutput(self, expected: str, *args: str) -> None:
|
43
44
|
"""Check the output from invoking a `python -m modulename` program matches the expected"""
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
actual = self.moduleOutput(*args)
|
46
|
+
self.assertMultiLineEqual(expected, actual, f"args={args}")
|
47
|
+
|
48
|
+
def assertStarts(self, expected: str, actual: str, *args: str) -> None:
|
49
|
+
self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}")
|
50
|
+
|
51
|
+
def assertModuleJson(self, expected: Any, *args: str) -> None:
|
52
|
+
"""Check the output from invoking a `python -m modulename` program matches the expected"""
|
53
|
+
actual = self.moduleOutput(*args)
|
54
|
+
self.assertEqual(json.loads(actual), expected)
|
47
55
|
|
48
56
|
def assertModuleOutputStarts(self, expected: str, *args: str) -> None:
|
49
57
|
"""Check the output from invoking a `python -m modulename` program matches the expected"""
|
50
|
-
|
51
|
-
|
52
|
-
)
|
58
|
+
actual = self.moduleOutput(*args)
|
59
|
+
self.assertStarts(expected, actual, *args)
|
53
60
|
|
54
61
|
def assertProgramError(self, *args: str) -> None:
|
55
62
|
"""Check the output from invoking a program fails"""
|
@@ -57,16 +64,12 @@ class CLITestCase(unittest.TestCase):
|
|
57
64
|
env["PYTHONIOENCODING"] = "utf-8"
|
58
65
|
with self.assertRaises(subprocess.SubprocessError):
|
59
66
|
subprocess.check_output(
|
60
|
-
args,
|
61
|
-
stdin=subprocess.DEVNULL,
|
62
|
-
stderr=subprocess.DEVNULL,
|
63
|
-
encoding="utf-8",
|
64
|
-
env=env,
|
67
|
+
args, stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, encoding="utf-8", env=env
|
65
68
|
)
|
66
69
|
|
67
70
|
def assertModuleError(self, *args: str) -> None:
|
68
71
|
"""Check the output from invoking a `python -m modulename` program fails"""
|
69
|
-
|
72
|
+
self.assertProgramError(*self.moduleArgs(*args))
|
70
73
|
|
71
74
|
def test_gen(self) -> None:
|
72
75
|
"""test wwvb.gen"""
|
@@ -153,10 +156,25 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
|
|
153
156
|
|
154
157
|
def test_json(self) -> None:
|
155
158
|
"""Test the JSON output format"""
|
156
|
-
self.
|
157
|
-
|
158
|
-
|
159
|
-
""
|
159
|
+
self.assertModuleJson(
|
160
|
+
[
|
161
|
+
{
|
162
|
+
"year": 2021,
|
163
|
+
"days": 340,
|
164
|
+
"hour": 3,
|
165
|
+
"minute": 40,
|
166
|
+
"amplitude": "210000000200000001120011001002000000010200010001020001000002",
|
167
|
+
"phase": "111110011011010101000100100110011110001110111010111101001011",
|
168
|
+
},
|
169
|
+
{
|
170
|
+
"year": 2021,
|
171
|
+
"days": 340,
|
172
|
+
"hour": 3,
|
173
|
+
"minute": 41,
|
174
|
+
"amplitude": "210000001200000001120011001002000000010200010001020001000002",
|
175
|
+
"phase": "001010011100100011000101110000100001101000001111101100000010",
|
176
|
+
},
|
177
|
+
],
|
160
178
|
"wwvb.gen",
|
161
179
|
"-m",
|
162
180
|
"2",
|
@@ -166,10 +184,23 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
|
|
166
184
|
"both",
|
167
185
|
"2021-12-6 3:40",
|
168
186
|
)
|
169
|
-
self.
|
170
|
-
|
171
|
-
|
172
|
-
""
|
187
|
+
self.assertModuleJson(
|
188
|
+
[
|
189
|
+
{
|
190
|
+
"year": 2021,
|
191
|
+
"days": 340,
|
192
|
+
"hour": 3,
|
193
|
+
"minute": 40,
|
194
|
+
"amplitude": "210000000200000001120011001002000000010200010001020001000002",
|
195
|
+
},
|
196
|
+
{
|
197
|
+
"year": 2021,
|
198
|
+
"days": 340,
|
199
|
+
"hour": 3,
|
200
|
+
"minute": 41,
|
201
|
+
"amplitude": "210000001200000001120011001002000000010200010001020001000002",
|
202
|
+
},
|
203
|
+
],
|
173
204
|
"wwvb.gen",
|
174
205
|
"-m",
|
175
206
|
"2",
|
@@ -179,10 +210,23 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
|
|
179
210
|
"amplitude",
|
180
211
|
"2021-12-6 3:40",
|
181
212
|
)
|
182
|
-
self.
|
183
|
-
|
184
|
-
|
185
|
-
""
|
213
|
+
self.assertModuleJson(
|
214
|
+
[
|
215
|
+
{
|
216
|
+
"year": 2021,
|
217
|
+
"days": 340,
|
218
|
+
"hour": 3,
|
219
|
+
"minute": 40,
|
220
|
+
"phase": "111110011011010101000100100110011110001110111010111101001011",
|
221
|
+
},
|
222
|
+
{
|
223
|
+
"year": 2021,
|
224
|
+
"days": 340,
|
225
|
+
"hour": 3,
|
226
|
+
"minute": 41,
|
227
|
+
"phase": "001010011100100011000101110000100001101000001111101100000010",
|
228
|
+
},
|
229
|
+
],
|
186
230
|
"wwvb.gen",
|
187
231
|
"-m",
|
188
232
|
"2",
|
@@ -198,9 +242,11 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0
|
|
198
242
|
self.assertModuleOutput(
|
199
243
|
"""\
|
200
244
|
WWVB timecode: year=2021 days=340 hour=03 min=40 dst=0 ut1=-100 ly=0 ls=0 --style=sextant
|
201
|
-
2021-340 03:40
|
245
|
+
2021-340 03:40 \
|
246
|
+
🬋🬩🬋🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬩🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬩🬹🬍🬎🬩🬹🬍🬎🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬎🬩🬹🬍🬎🬋🬎🬩🬹🬩🬹🬋🬍🬍🬎🬩🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬋🬎🬩🬹🬋🬩🬩🬹🬍🬎🬩🬹🬋🬹🬩🬹🬍🬎🬩🬹🬋🬎🬩🬹🬋🬩🬩🬹🬩🬹🬍🬎🬋🬹🬍🬎🬍🬎🬩🬹🬍🬎🬩🬹🬋🬩
|
202
247
|
|
203
|
-
2021-340 03:41
|
248
|
+
2021-340 03:41 \
|
249
|
+
🬋🬍🬋🬎🬩🬹🬍🬎🬩🬹🬍🬎🬍🬎🬩🬹🬋🬹🬋🬩🬍🬎🬍🬎🬩🬹🬍🬎🬍🬎🬍🬎🬩🬹🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬹🬩🬹🬩🬹🬋🬎🬍🬎🬍🬎🬋🬍🬩🬹🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬩🬹🬋🬎🬩🬹🬋🬍🬍🬎🬍🬎🬍🬎🬋🬎🬩🬹🬩🬹🬩🬹🬋🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬋🬎🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬋🬍
|
204
250
|
|
205
251
|
""",
|
206
252
|
"wwvb.gen",
|
wwvb/testdaylight.py
CHANGED
@@ -19,9 +19,7 @@ class TestDaylight(unittest.TestCase):
|
|
19
19
|
"""Test that the onset of DST is the same in Mountain and WWVBMinute (which uses ls bits)"""
|
20
20
|
for h in [8, 9, 10]:
|
21
21
|
for dm in range(-1441, 1442):
|
22
|
-
d = datetime.datetime(
|
23
|
-
2021, 3, 14, h, 0, tzinfo=datetime.timezone.utc
|
24
|
-
) + datetime.timedelta(minutes=dm)
|
22
|
+
d = datetime.datetime(2021, 3, 14, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
|
25
23
|
m = wwvb.WWVBMinute.from_datetime(d)
|
26
24
|
self.assertEqual(
|
27
25
|
m.as_datetime_local().replace(tzinfo=Mountain),
|
@@ -32,9 +30,7 @@ class TestDaylight(unittest.TestCase):
|
|
32
30
|
"""Test that the end of DST is the same in Mountain and WWVBMinute (which uses ls bits)"""
|
33
31
|
for h in [7, 8, 9]:
|
34
32
|
for dm in range(-1441, 1442):
|
35
|
-
d = datetime.datetime(
|
36
|
-
2021, 11, 7, h, 0, tzinfo=datetime.timezone.utc
|
37
|
-
) + datetime.timedelta(minutes=dm)
|
33
|
+
d = datetime.datetime(2021, 11, 7, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
|
38
34
|
m = wwvb.WWVBMinute.from_datetime(d)
|
39
35
|
self.assertEqual(
|
40
36
|
m.as_datetime_local().replace(tzinfo=Mountain),
|
@@ -45,9 +41,7 @@ class TestDaylight(unittest.TestCase):
|
|
45
41
|
"""Test that middle of DST is the same in Mountain and WWVBMinute (which uses ls bits)"""
|
46
42
|
for h in [7, 8, 9]:
|
47
43
|
for dm in (-1, 0, 1):
|
48
|
-
d = datetime.datetime(
|
49
|
-
2021, 7, 7, h, 0, tzinfo=datetime.timezone.utc
|
50
|
-
) + datetime.timedelta(minutes=dm)
|
44
|
+
d = datetime.datetime(2021, 7, 7, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
|
51
45
|
m = wwvb.WWVBMinute.from_datetime(d)
|
52
46
|
self.assertEqual(
|
53
47
|
m.as_datetime_local().replace(tzinfo=Mountain),
|
@@ -58,9 +52,7 @@ class TestDaylight(unittest.TestCase):
|
|
58
52
|
"""Test that middle of standard time is the same in Mountain and WWVBMinute (which uses ls bits)"""
|
59
53
|
for h in [7, 8, 9]:
|
60
54
|
for dm in (-1, 0, 1):
|
61
|
-
d = datetime.datetime(
|
62
|
-
2021, 12, 25, h, 0, tzinfo=datetime.timezone.utc
|
63
|
-
) + datetime.timedelta(minutes=dm)
|
55
|
+
d = datetime.datetime(2021, 12, 25, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm)
|
64
56
|
m = wwvb.WWVBMinute.from_datetime(d)
|
65
57
|
self.assertEqual(
|
66
58
|
m.as_datetime_local().replace(tzinfo=Mountain),
|
wwvb/testls.py
CHANGED
@@ -55,9 +55,7 @@ class TestLeapSecond(unittest.TestCase):
|
|
55
55
|
leap.append(nm)
|
56
56
|
else:
|
57
57
|
assert not our_is_ls
|
58
|
-
d = datetime.datetime.combine(nm, datetime.time()).replace(
|
59
|
-
tzinfo=datetime.timezone.utc
|
60
|
-
)
|
58
|
+
d = datetime.datetime.combine(nm, datetime.time()).replace(tzinfo=datetime.timezone.utc)
|
61
59
|
self.assertEqual(leap, bench)
|
62
60
|
|
63
61
|
|
wwvb/testpm.py
CHANGED
@@ -15,23 +15,9 @@ class TestPhaseModulation(unittest.TestCase):
|
|
15
15
|
|
16
16
|
def test_pm(self) -> None:
|
17
17
|
"""Compare the generated signal from a reference minute in NIST docs"""
|
18
|
-
ref_am =
|
19
|
-
|
20
|
-
|
21
|
-
"0001010002"
|
22
|
-
"0110001012"
|
23
|
-
"0100000012"
|
24
|
-
"0010010112"
|
25
|
-
)
|
26
|
-
|
27
|
-
ref_pm = (
|
28
|
-
"0011101101"
|
29
|
-
"0001001000"
|
30
|
-
"0011001000"
|
31
|
-
"0110001101"
|
32
|
-
"0011010001"
|
33
|
-
"0110110110"
|
34
|
-
)
|
18
|
+
ref_am = "2011000002" "0001001112" "0001010002" "0110001012" "0100000012" "0010010112"
|
19
|
+
|
20
|
+
ref_pm = "0011101101" "0001001000" "0011001000" "0110001101" "0011010001" "0110110110"
|
35
21
|
|
36
22
|
ref_minute = wwvb.WWVBMinuteIERS(2012, 186, 17, 30, dst=3)
|
37
23
|
ref_time = ref_minute.as_timecode()
|
wwvb/testuwwvb.py
CHANGED
@@ -21,10 +21,10 @@ EitherDatetimeOrNone = Union[None, datetime.datetime, adafruit_datetime.datetime
|
|
21
21
|
class WWVBRoundtrip(unittest.TestCase):
|
22
22
|
"""tests of uwwvb.py"""
|
23
23
|
|
24
|
-
def assertDateTimeEqualExceptTzInfo(
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
def assertDateTimeEqualExceptTzInfo(self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone) -> None:
|
25
|
+
"""Test two datetime objects for equality
|
26
|
+
|
27
|
+
This equality test excludes tzinfo, and allows adafruit_datetime and core datetime modules to compare equal"""
|
28
28
|
assert a
|
29
29
|
assert b
|
30
30
|
self.assertEqual(
|
@@ -35,9 +35,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
35
35
|
def test_decode(self) -> None:
|
36
36
|
"""Test decoding of some minutes including a leap second.
|
37
37
|
Each minute must decode and match the primary decoder."""
|
38
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
39
|
-
datetime.datetime(2012, 6, 30, 23, 50)
|
40
|
-
)
|
38
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
41
39
|
assert minute
|
42
40
|
decoder = uwwvb.WWVBDecoder()
|
43
41
|
decoder.update(uwwvb.MARK)
|
@@ -60,17 +58,13 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
60
58
|
def test_roundtrip(self) -> None:
|
61
59
|
"""Test that some big range of times all decode the same as the primary decoder"""
|
62
60
|
dt = datetime.datetime(2002, 1, 1, 0, 0)
|
63
|
-
delta = datetime.timedelta(
|
64
|
-
minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182
|
65
|
-
)
|
61
|
+
delta = datetime.timedelta(minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182)
|
66
62
|
while dt.year < 2013:
|
67
63
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
68
64
|
assert minute
|
69
65
|
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
|
70
66
|
assert decoded
|
71
|
-
self.assertDateTimeEqualExceptTzInfo(
|
72
|
-
minute.as_datetime_utc(), uwwvb.as_datetime_utc(decoded)
|
73
|
-
)
|
67
|
+
self.assertDateTimeEqualExceptTzInfo(minute.as_datetime_utc(), uwwvb.as_datetime_utc(decoded))
|
74
68
|
dt = dt + delta
|
75
69
|
|
76
70
|
def test_dst(self) -> None:
|
@@ -93,9 +87,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
93
87
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
94
88
|
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
|
95
89
|
assert decoded
|
96
|
-
self.assertDateTimeEqualExceptTzInfo(
|
97
|
-
minute.as_datetime_local(), uwwvb.as_datetime_local(decoded)
|
98
|
-
)
|
90
|
+
self.assertDateTimeEqualExceptTzInfo(minute.as_datetime_local(), uwwvb.as_datetime_local(decoded))
|
99
91
|
|
100
92
|
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
|
101
93
|
assert decoded
|
@@ -106,9 +98,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
106
98
|
|
107
99
|
def test_noise(self) -> None:
|
108
100
|
"""Test of the state-machine decoder when faced with pseudorandom noise"""
|
109
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
110
|
-
datetime.datetime(2012, 6, 30, 23, 50)
|
111
|
-
)
|
101
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
112
102
|
r = random.Random(408)
|
113
103
|
junk = [
|
114
104
|
r.choice(
|
@@ -141,9 +131,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
141
131
|
|
142
132
|
def test_noise2(self) -> None:
|
143
133
|
"""Test of the full minute decoder with targeted errors to get full coverage"""
|
144
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
145
|
-
datetime.datetime(2012, 6, 30, 23, 50)
|
146
|
-
)
|
134
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
147
135
|
timecode = minute.as_timecode()
|
148
136
|
decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am])
|
149
137
|
self.assertIsNotNone(decoded)
|
@@ -178,9 +166,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
178
166
|
|
179
167
|
def test_noise3(self) -> None:
|
180
168
|
"""Test impossible BCD values"""
|
181
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
182
|
-
datetime.datetime(2012, 6, 30, 23, 50)
|
183
|
-
)
|
169
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
184
170
|
timecode = minute.as_timecode()
|
185
171
|
|
186
172
|
for poslist in [
|
wwvb/testwwvb.py
CHANGED
@@ -27,7 +27,6 @@ class WWVBMinute2k(wwvb.WWVBMinute):
|
|
27
27
|
epoch = 2000
|
28
28
|
|
29
29
|
|
30
|
-
# pylint: disable=too-many-locals
|
31
30
|
class WWVBTestCase(unittest.TestCase):
|
32
31
|
"""Test each expected output in tests/. Some outputs are from another program, some are from us"""
|
33
32
|
|
@@ -85,9 +84,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
85
84
|
|
86
85
|
def test_decode(self) -> None:
|
87
86
|
"""Test that a range of minutes including a leap second are correctly decoded by the state-based decoder"""
|
88
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
89
|
-
datetime.datetime(1992, 6, 30, 23, 50)
|
90
|
-
)
|
87
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
91
88
|
decoder = decode.wwvbreceive()
|
92
89
|
next(decoder)
|
93
90
|
decoder.send(wwvb.AmplitudeModulation.MARK)
|
@@ -126,17 +123,13 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
126
123
|
def test_roundtrip(self) -> None:
|
127
124
|
"""Test that a wide of minutes are correctly decoded by the state-based decoder"""
|
128
125
|
dt = datetime.datetime(1992, 1, 1, 0, 0)
|
129
|
-
delta = datetime.timedelta(
|
130
|
-
minutes=915 if sys.implementation.name == "cpython" else 86400 - 915
|
131
|
-
)
|
126
|
+
delta = datetime.timedelta(minutes=915 if sys.implementation.name == "cpython" else 86400 - 915)
|
132
127
|
while dt.year < 1993:
|
133
128
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
134
129
|
assert minute is not None
|
135
130
|
timecode = minute.as_timecode().am
|
136
131
|
assert timecode
|
137
|
-
decoded_minute: Optional[
|
138
|
-
wwvb.WWVBMinute
|
139
|
-
] = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode())
|
132
|
+
decoded_minute: Optional[wwvb.WWVBMinute] = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode())
|
140
133
|
assert decoded_minute
|
141
134
|
decoded = decoded_minute.as_timecode().am
|
142
135
|
self.assertEqual(
|
@@ -148,9 +141,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
148
141
|
|
149
142
|
def test_noise(self) -> None:
|
150
143
|
"""Test against pseudorandom noise"""
|
151
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
152
|
-
datetime.datetime(1992, 6, 30, 23, 50)
|
153
|
-
)
|
144
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
154
145
|
r = random.Random(408)
|
155
146
|
junk = [
|
156
147
|
r.choice(
|
@@ -180,9 +171,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
180
171
|
|
181
172
|
def test_noise2(self) -> None:
|
182
173
|
"""Test of the full minute decoder with targeted errors to get full coverage"""
|
183
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
184
|
-
datetime.datetime(2012, 6, 30, 23, 50)
|
185
|
-
)
|
174
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
186
175
|
timecode = minute.as_timecode()
|
187
176
|
decoded = wwvb.WWVBMinute.from_timecode_am(timecode)
|
188
177
|
self.assertIsNotNone(decoded)
|
@@ -217,9 +206,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
217
206
|
|
218
207
|
def test_noise3(self) -> None:
|
219
208
|
"""Test impossible BCD values"""
|
220
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
221
|
-
datetime.datetime(2012, 6, 30, 23, 50)
|
222
|
-
)
|
209
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
223
210
|
timecode = minute.as_timecode()
|
224
211
|
|
225
212
|
for poslist in [
|
@@ -241,16 +228,12 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
241
228
|
|
242
229
|
def test_previous_next_minute(self) -> None:
|
243
230
|
"""Test that previous minute and next minute are inverses"""
|
244
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
245
|
-
datetime.datetime(1992, 6, 30, 23, 50)
|
246
|
-
)
|
231
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
247
232
|
self.assertEqual(minute, minute.next_minute().previous_minute())
|
248
233
|
|
249
234
|
def test_timecode_str(self) -> None:
|
250
235
|
"""Test the str() and repr() methods"""
|
251
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
252
|
-
datetime.datetime(1992, 6, 30, 23, 50)
|
253
|
-
)
|
236
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
254
237
|
timecode = minute.as_timecode()
|
255
238
|
self.assertEqual(
|
256
239
|
str(timecode),
|
@@ -268,9 +251,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
268
251
|
sm1 = s - datetime.timedelta(days=1)
|
269
252
|
self.assertEqual(wwvb.get_dut1(s), wwvb.get_dut1(sm1))
|
270
253
|
|
271
|
-
e = iersdata.DUT1_DATA_START + datetime.timedelta(
|
272
|
-
days=len(iersdata.DUT1_OFFSETS) - 1
|
273
|
-
)
|
254
|
+
e = iersdata.DUT1_DATA_START + datetime.timedelta(days=len(iersdata.DUT1_OFFSETS) - 1)
|
274
255
|
ep1 = e + datetime.timedelta(days=1)
|
275
256
|
|
276
257
|
self.assertEqual(wwvb.get_dut1(e), wwvb.get_dut1(ep1))
|
@@ -290,17 +271,11 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
290
271
|
|
291
272
|
s = "WWVB timecode: year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
|
292
273
|
t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
|
293
|
-
self.assertEqual(
|
294
|
-
wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)
|
295
|
-
)
|
274
|
+
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
|
296
275
|
t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ls=1"
|
297
|
-
self.assertEqual(
|
298
|
-
wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)
|
299
|
-
)
|
276
|
+
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
|
300
277
|
t = "year=1998 days=365 hour=23 min=56 dst=0"
|
301
|
-
self.assertEqual(
|
302
|
-
wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)
|
303
|
-
)
|
278
|
+
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
|
304
279
|
|
305
280
|
def test_from_datetime(self) -> None:
|
306
281
|
"""Test the from_datetime() classmethod"""
|
@@ -322,9 +297,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
322
297
|
wwvb.WWVBMinute(2021, 1, 1, 1, ls=False)
|
323
298
|
|
324
299
|
with self.assertRaises(ValueError):
|
325
|
-
wwvb.WWVBMinute.fromstring(
|
326
|
-
"year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1"
|
327
|
-
)
|
300
|
+
wwvb.WWVBMinute.fromstring("year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1")
|
328
301
|
|
329
302
|
def test_deprecated(self) -> None:
|
330
303
|
"""Ensure that the 'maybe_warn_update' function is covered"""
|
@@ -357,25 +330,19 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
357
330
|
wwvb.get_dst_next(datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba")),
|
358
331
|
0b101111,
|
359
332
|
)
|
360
|
-
date, row = wwvb.get_dst_change_date_and_row(
|
361
|
-
datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba")
|
362
|
-
)
|
333
|
+
date, row = wwvb.get_dst_change_date_and_row(datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba"))
|
363
334
|
self.assertIsNone(date)
|
364
335
|
self.assertIsNone(row)
|
365
336
|
|
366
337
|
# California was weird in 1948
|
367
338
|
self.assertEqual(
|
368
|
-
wwvb.get_dst_next(
|
369
|
-
datetime.datetime(1948, 1, 1), tz=tz.ZoneInfo("America/Los_Angeles")
|
370
|
-
),
|
339
|
+
wwvb.get_dst_next(datetime.datetime(1948, 1, 1), tz=tz.ZoneInfo("America/Los_Angeles")),
|
371
340
|
0b100011,
|
372
341
|
)
|
373
342
|
|
374
343
|
# Berlin had DST changes on Monday in 1917
|
375
344
|
self.assertEqual(
|
376
|
-
wwvb.get_dst_next(
|
377
|
-
datetime.datetime(1917, 1, 1), tz=tz.ZoneInfo("Europe/Berlin")
|
378
|
-
),
|
345
|
+
wwvb.get_dst_next(datetime.datetime(1917, 1, 1), tz=tz.ZoneInfo("Europe/Berlin")),
|
379
346
|
0b100011,
|
380
347
|
)
|
381
348
|
|
@@ -383,9 +350,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
383
350
|
# Australia observes DST in the other half of the year compared to the
|
384
351
|
# Northern hemisphere
|
385
352
|
self.assertEqual(
|
386
|
-
wwvb.get_dst_next(
|
387
|
-
datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Australia/Melbourne")
|
388
|
-
),
|
353
|
+
wwvb.get_dst_next(datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Australia/Melbourne")),
|
389
354
|
0b100011,
|
390
355
|
)
|
391
356
|
|
wwvb/updateiers.py
CHANGED
@@ -23,15 +23,12 @@ DIST_PATH = str(pathlib.Path(__file__).parent / "iersdata_dist.py")
|
|
23
23
|
|
24
24
|
OLD_TABLE_START: Optional[datetime.date] = None
|
25
25
|
OLD_TABLE_END: Optional[datetime.date] = None
|
26
|
-
|
26
|
+
if os.path.exists(DIST_PATH):
|
27
27
|
import wwvb.iersdata_dist
|
28
28
|
|
29
29
|
OLD_TABLE_START = wwvb.iersdata_dist.DUT1_DATA_START
|
30
|
-
OLD_TABLE_END = OLD_TABLE_START + datetime.timedelta(
|
31
|
-
|
32
|
-
)
|
33
|
-
except (ImportError, NameError) as e:
|
34
|
-
pass
|
30
|
+
OLD_TABLE_END = OLD_TABLE_START + datetime.timedelta(days=len(wwvb.iersdata_dist.DUT1_OFFSETS) - 1)
|
31
|
+
|
35
32
|
IERS_URL = "https://datacenter.iers.org/data/csv/finals2000A.all.csv"
|
36
33
|
if os.path.exists("finals2000A.all.csv"):
|
37
34
|
IERS_URL = "finals2000A.all.csv"
|
@@ -48,7 +45,7 @@ def _get_text(url: str) -> str:
|
|
48
45
|
return open(url, encoding="utf-8").read()
|
49
46
|
|
50
47
|
|
51
|
-
def update_iersdata(
|
48
|
+
def update_iersdata(
|
52
49
|
target_file: str,
|
53
50
|
) -> None:
|
54
51
|
"""Update iersdata.py"""
|
@@ -93,11 +90,7 @@ def update_iersdata( # pylint: disable=too-many-locals, too-many-branches, too-
|
|
93
90
|
assert wwvb_dut1_table
|
94
91
|
meta = wwvb_data.find("meta", property="article:modified_time")
|
95
92
|
assert isinstance(meta, bs4.Tag)
|
96
|
-
wwvb_data_stamp = (
|
97
|
-
datetime.datetime.fromisoformat(meta.attrs["content"])
|
98
|
-
.replace(tzinfo=None)
|
99
|
-
.date()
|
100
|
-
)
|
93
|
+
wwvb_data_stamp = datetime.datetime.fromisoformat(meta.attrs["content"]).replace(tzinfo=None).date()
|
101
94
|
|
102
95
|
def patch(patch_start: datetime.date, patch_end: datetime.date, val: int) -> None:
|
103
96
|
off_start = (patch_start - table_start).days
|
@@ -137,21 +130,18 @@ def update_iersdata( # pylint: disable=too-many-locals, too-many-branches, too-
|
|
137
130
|
print(*args, file=output)
|
138
131
|
|
139
132
|
code("# -*- python3 -*-")
|
133
|
+
code("# fmt: off")
|
140
134
|
code('"""File generated from public data - not subject to copyright"""')
|
141
135
|
code("# SPDX" + "-FileCopyrightText: Public domain")
|
142
136
|
code("# SPDX" + "-License-Identifier: CC0-1.0")
|
143
|
-
code("# fmt: off")
|
144
137
|
code("# isort: skip_file")
|
145
|
-
code("# pylint: disable=invalid-name")
|
146
138
|
code("import datetime")
|
147
139
|
|
148
140
|
code("__all__ = ['DUT1_DATA_START', 'DUT1_OFFSETS']")
|
149
141
|
code(f"DUT1_DATA_START = {repr(table_start)}")
|
150
142
|
c = sorted(chr(ord("a") + ch + 10) for ch in set(offsets))
|
151
143
|
code(f"{','.join(c)} = tuple({repr(''.join(c))})")
|
152
|
-
code(
|
153
|
-
f"DUT1_OFFSETS = str( # {table_start.year:04d}{table_start.month:02d}{table_start.day:02d}"
|
154
|
-
)
|
144
|
+
code(f"DUT1_OFFSETS = str( # {table_start.year:04d}{table_start.month:02d}{table_start.day:02d}")
|
155
145
|
line = ""
|
156
146
|
j = 0
|
157
147
|
|
@@ -194,9 +184,7 @@ def iersdata_path(callback: Callable[[str, str], str]) -> str:
|
|
194
184
|
default=iersdata_path(platformdirs.user_data_dir),
|
195
185
|
)
|
196
186
|
@click.option("--dist", "location", flag_value=DIST_PATH)
|
197
|
-
@click.option(
|
198
|
-
"--site", "location", flag_value=iersdata_path(platformdirs.site_data_dir)
|
199
|
-
)
|
187
|
+
@click.option("--site", "location", flag_value=iersdata_path(platformdirs.site_data_dir))
|
200
188
|
def main(location: str) -> None:
|
201
189
|
"""Update DUT1 data"""
|
202
190
|
print("will write to", location)
|
wwvb/wwvbtk.py
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
import functools
|
10
10
|
import threading
|
11
11
|
import time
|
12
|
-
from tkinter import Canvas, TclError, Tk
|
12
|
+
from tkinter import Canvas, TclError, Tk
|
13
13
|
from typing import Any, Generator, Optional, Tuple
|
14
14
|
|
15
15
|
import click
|
@@ -23,9 +23,7 @@ def _app() -> Tk:
|
|
23
23
|
return Tk()
|
24
24
|
|
25
25
|
|
26
|
-
def validate_colors(
|
27
|
-
ctx: Any, param: Any, value: str
|
28
|
-
) -> list[str]:
|
26
|
+
def validate_colors(ctx: Any, param: Any, value: str) -> list[str]:
|
29
27
|
"""Check that all colors in a string are valid, splitting it to a list"""
|
30
28
|
app = _app()
|
31
29
|
colors = value.split()
|
@@ -79,9 +77,7 @@ def main(colors: list[str], size: int, min_size: Optional[int]) -> None:
|
|
79
77
|
yield timestamp + i, code
|
80
78
|
timestamp = timestamp + 60
|
81
79
|
|
82
|
-
def wwvbsmarttick() ->
|
83
|
-
Generator[Tuple[float, wwvb.AmplitudeModulation], None, None]
|
84
|
-
):
|
80
|
+
def wwvbsmarttick() -> Generator[Tuple[float, wwvb.AmplitudeModulation], None, None]:
|
85
81
|
"""Yield consecutive values of the WWVB amplitude signal but deal with time
|
86
82
|
progressing unexpectedly, such as when the computer is suspended or NTP steps
|
87
83
|
the clock backwards
|
@@ -0,0 +1,23 @@
|
|
1
|
+
uwwvb.py,sha256=UX5oSMs4KZPt1hg1urZXhN0pMU3BvQWYOKExZT8FiIo,5674
|
2
|
+
wwvb/__init__.py,sha256=oUUcweJnrFDVw5_-smJopfVAk5yNFlEnOkYMixNxZzc,29762
|
3
|
+
wwvb/__version__.py,sha256=UsWpfolO3V5lhkRSzLYHaglRPoxe1Nfcc4kMi3j-8W8,411
|
4
|
+
wwvb/decode.py,sha256=PBfBzYJJkIh8VD-d_SHvBl3gX4FpXhqG1lH2fP8lUaI,2758
|
5
|
+
wwvb/dut1table.py,sha256=uqaCnCOWr7ytx-nt3mmyhFb9jJVNP5N-WJX-glunKAk,890
|
6
|
+
wwvb/gen.py,sha256=pOSgkJTh_TDKN1FulzbAW8aN77bJXtJjo1KGoBEU3Dk,3697
|
7
|
+
wwvb/iersdata.py,sha256=hJFj-ZL3RP13dcdFGWoT6WOgoV-6CAy-sWSYfi_EXwg,965
|
8
|
+
wwvb/iersdata_dist.py,sha256=dmd6ShKPG5AHBm0dfRAKFpFpt8WDiW1NqNF8DLeMgrI,2363
|
9
|
+
wwvb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
wwvb/testcli.py,sha256=06uIEdg-ZH0UOISr0X7oy4iAQ5eb4-MejQC7TeqIJz8,10086
|
11
|
+
wwvb/testdaylight.py,sha256=JW8UJK-FeAg9Kjy5F_aBYbUVj44DKpJOXQ-u9ooyprA,2485
|
12
|
+
wwvb/testls.py,sha256=Kz4-MWLaUKABwyql8sWdzvtg8gipxhHv4r-6fn3fptg,1720
|
13
|
+
wwvb/testpm.py,sha256=I5ajzZjUypyKszSe4sTnMZbS43nPmee4H1G3uZlHC_w,935
|
14
|
+
wwvb/testuwwvb.py,sha256=oj68-Vv1EFBG9MOWB8zW2sTN1Yg22nVYOL_Aor3IJTI,8398
|
15
|
+
wwvb/testwwvb.py,sha256=bWMI0-Qbaye9Afil4qyPKsNYtY-B6d1wBJZUn-MS99w,16109
|
16
|
+
wwvb/tz.py,sha256=XVYh0btrnyP_nUiZUwBjufkYbb26_DTiZVl-R_1BA2A,299
|
17
|
+
wwvb/updateiers.py,sha256=zpMvIzuPvKdA-XvBcOwQiFx7e9yH-V-VypL95bG5rWk,7240
|
18
|
+
wwvb/wwvbtk.py,sha256=fFXoyReyGZIEpddz74KY0vKfLm1ZP1jwDHTTAbVd8XY,4525
|
19
|
+
wwvb-3.0.8.dist-info/METADATA,sha256=99ps6bN93tu3nAzKIVWepexqDDaPtfeg_SR4a0c2fRI,10290
|
20
|
+
wwvb-3.0.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
21
|
+
wwvb-3.0.8.dist-info/entry_points.txt,sha256=KSevvHWLEKxOxUQ-L-OQidD4Sj2BPEfhZ2TQhOgyys4,179
|
22
|
+
wwvb-3.0.8.dist-info/top_level.txt,sha256=0IYdkhEAMgurpv_F-76rlyn4GdxepGFzG99tivVdQVU,11
|
23
|
+
wwvb-3.0.8.dist-info/RECORD,,
|
wwvb-3.0.6.dist-info/RECORD
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
uwwvb.py,sha256=Fiym40qquFafHSpOjetQqlYEJHN65dFOdRTVp3MLodA,5718
|
2
|
-
wwvb/__init__.py,sha256=r5qzreH70tOD5N-FrR5aI5HTA3BoWuZWgHwi-sevHhM,30058
|
3
|
-
wwvb/__version__.py,sha256=uNMH75I1pte97qzohjB8IClzvqSWDoMr4D9lZLC76js,411
|
4
|
-
wwvb/decode.py,sha256=7x_3ThwcROZflmpCwW1AT1zAIM5Q1wzuhr2cEz_I--w,2851
|
5
|
-
wwvb/dut1table.py,sha256=AG_pbfbaM1tOnudx0coFjLQ4JnoVnTCW6Ki0c-tH79Q,889
|
6
|
-
wwvb/gen.py,sha256=RQOCk5gPuA01rpOkDO634Yae7LnMe69tqDvf3JQZvfQ,3850
|
7
|
-
wwvb/iersdata.py,sha256=5MtuRHtmtd75Kht3j0io8A33gx-_ELew7fux1Mp1VPA,1000
|
8
|
-
wwvb/iersdata_dist.py,sha256=ViVd6QR4_ktWax5dHLdu-c69bIrBruWY_AYt7WBYdgs,2394
|
9
|
-
wwvb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
wwvb/testcli.py,sha256=O-B3wchI11MpiAvLr-nBQ-HMCR0OIauWsKZWSjB6zcg,8751
|
11
|
-
wwvb/testdaylight.py,sha256=jr9i2SmIn49x4tfqKJtUoQzNV_jgtjbHJu3B_wzBeNU,2637
|
12
|
-
wwvb/testls.py,sha256=9E94nEqvTI2yyp9ssYrTfjzZ2vxsOud9sGT_Ba4CJ18,1750
|
13
|
-
wwvb/testpm.py,sha256=10_oPNLn5mF5tVXbZ5uruZpNiOIJcyIWCWLKzv-P5oo,1103
|
14
|
-
wwvb/testuwwvb.py,sha256=87xDJH-GWFip6SN5CEhmRS0UrA3YFw0KG6pO3iVZIZs,8590
|
15
|
-
wwvb/testwwvb.py,sha256=bJH8BHT8WA35ys--tM63wL3rdPyOfLfcRWK0uVhyxko,16557
|
16
|
-
wwvb/tz.py,sha256=XVYh0btrnyP_nUiZUwBjufkYbb26_DTiZVl-R_1BA2A,299
|
17
|
-
wwvb/updateiers.py,sha256=T5tiwnEBAJgAVmjMBi8OCf0zgjPdasN8SmJxa6oDrmY,7459
|
18
|
-
wwvb/wwvbtk.py,sha256=sP34Ebb0_3alri0GCorK5o0IBtFTQcIZi3lh9MgOiyo,4614
|
19
|
-
wwvb-3.0.6.dist-info/METADATA,sha256=l41waOVz7AOMTr3PUBNe7sFwM8IS_tDSbYd-WROw1XU,10290
|
20
|
-
wwvb-3.0.6.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
21
|
-
wwvb-3.0.6.dist-info/entry_points.txt,sha256=KSevvHWLEKxOxUQ-L-OQidD4Sj2BPEfhZ2TQhOgyys4,179
|
22
|
-
wwvb-3.0.6.dist-info/top_level.txt,sha256=0IYdkhEAMgurpv_F-76rlyn4GdxepGFzG99tivVdQVU,11
|
23
|
-
wwvb-3.0.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|