wwvb 3.0.8__py3-none-any.whl → 4.0.0__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.
wwvb/__init__.py CHANGED
@@ -6,25 +6,24 @@
6
6
  #
7
7
  # SPDX-License-Identifier: GPL-3.0-only
8
8
 
9
- import collections
9
+ from __future__ import annotations
10
+
10
11
  import datetime
11
12
  import enum
12
13
  import json
13
14
  import warnings
14
- from typing import Dict, Generator, List, Optional, TextIO, Tuple, TypeVar, Union
15
+ from typing import Any, Generator, NamedTuple, TextIO, TypeVar
15
16
 
16
17
  from . import iersdata
17
18
  from .tz import Mountain
18
19
 
19
20
  HOUR = datetime.timedelta(seconds=3600)
20
21
  SECOND = datetime.timedelta(seconds=1)
21
- DateOrDatetime = TypeVar("DateOrDatetime", datetime.date, datetime.datetime)
22
22
  T = TypeVar("T")
23
23
 
24
24
 
25
- def require(x: Optional[T]) -> T:
26
- """Assert that an Optional value is not None, and then return the value,
27
- giving a hint to the type system."""
25
+ def _require(x: T | None) -> T:
26
+ """Check an Optional item is not None."""
28
27
  assert x is not None
29
28
  return x
30
29
 
@@ -35,24 +34,28 @@ def _removeprefix(s: str, p: str) -> str:
35
34
  return s
36
35
 
37
36
 
38
- def _date(dt: DateOrDatetime) -> datetime.date:
37
+ def _date(dt: datetime.date) -> datetime.date:
39
38
  """Return the date object itself, or the date property of a datetime"""
40
39
  if isinstance(dt, datetime.datetime):
41
40
  return dt.date()
42
41
  return dt
43
42
 
44
43
 
45
- def _maybe_warn_update(dt: datetime.date) -> None:
44
+ def _maybe_warn_update(dt: datetime.date, stacklevel: int = 1) -> None:
46
45
  """Maybe print a notice to run updateiers, if it seems useful to do so."""
47
46
  # We already know this date is not covered.
48
- # If the date is less than 330 days after today, there should be (possibly)
47
+ # If the date is less than 300 days after today, there should be (possibly)
49
48
  # prospective available now.
50
- today = datetime.date.today()
49
+ today = datetime.datetime.now(tz=datetime.timezone.utc).date()
50
+ print(f"_mwu {today=!r} {dt=!r} {iersdata.end=!r}")
51
51
  if _date(dt) < today + datetime.timedelta(days=330):
52
- warnings.warn("Note: Running `updateiers` may provide better DUT1 and LS information")
52
+ warnings.warn(
53
+ "Note: Running `updateiers` may provide better DUT1 and LS information",
54
+ stacklevel=stacklevel + 1,
55
+ )
53
56
 
54
57
 
55
- def get_dut1(dt: DateOrDatetime, *, warn_outdated: bool = True) -> float:
58
+ def get_dut1(dt: datetime.date, *, warn_outdated: bool = True) -> float:
56
59
  """Return the DUT1 number for the given timestamp"""
57
60
  date = _date(dt)
58
61
  i = (date - iersdata.DUT1_DATA_START).days
@@ -60,7 +63,7 @@ def get_dut1(dt: DateOrDatetime, *, warn_outdated: bool = True) -> float:
60
63
  v = iersdata.DUT1_OFFSETS[0]
61
64
  elif i >= len(iersdata.DUT1_OFFSETS):
62
65
  if warn_outdated:
63
- _maybe_warn_update(dt)
66
+ _maybe_warn_update(dt, stacklevel=2)
64
67
  v = iersdata.DUT1_OFFSETS[-1]
65
68
  else:
66
69
  v = iersdata.DUT1_OFFSETS[i]
@@ -74,7 +77,7 @@ def isly(year: int) -> bool:
74
77
  return d1.year == d2.year
75
78
 
76
79
 
77
- def isls(t: DateOrDatetime) -> bool:
80
+ def isls(t: datetime.date) -> bool:
78
81
  """Return True if a leap second occurs at the end of this month"""
79
82
  dut1_today = get_dut1(t)
80
83
  month_today = t.month
@@ -86,11 +89,11 @@ def isls(t: DateOrDatetime) -> bool:
86
89
 
87
90
  def isdst(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
88
91
  """Return true if daylight saving time is active at the start of the given UTC day"""
89
- t = datetime.datetime(t.year, t.month, t.day, tzinfo=datetime.timezone.utc)
90
- return bool(t.astimezone(tz).dst())
92
+ utc_daystart = datetime.datetime(t.year, t.month, t.day, tzinfo=datetime.timezone.utc)
93
+ return bool(utc_daystart.astimezone(tz).dst())
91
94
 
92
95
 
93
- def first_sunday_on_or_after(dt: DateOrDatetime) -> DateOrDatetime:
96
+ def _first_sunday_on_or_after(dt: datetime.date) -> datetime.date:
94
97
  """Return the first sunday on or after the reference time"""
95
98
  days_to_go = 6 - dt.weekday()
96
99
  if days_to_go:
@@ -98,17 +101,17 @@ def first_sunday_on_or_after(dt: DateOrDatetime) -> DateOrDatetime:
98
101
  return dt
99
102
 
100
103
 
101
- def first_sunday_in_month(y: int, m: int) -> datetime.date:
104
+ def _first_sunday_in_month(y: int, m: int) -> datetime.date:
102
105
  """Find the first sunday in a given month"""
103
- return first_sunday_on_or_after(datetime.datetime(y, m, 1))
106
+ return _first_sunday_on_or_after(datetime.datetime(y, m, 1, tzinfo=datetime.timezone.utc))
104
107
 
105
108
 
106
- def is_dst_change_day(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
109
+ def _is_dst_change_day(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
107
110
  """Return True if the day is a DST change day"""
108
111
  return isdst(t, tz) != isdst(t + datetime.timedelta(1), tz)
109
112
 
110
113
 
111
- def get_dst_change_hour(t: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> Optional[int]:
114
+ def _get_dst_change_hour(t: datetime.date, tz: datetime.tzinfo = Mountain) -> int | None:
112
115
  """Return the hour when DST changes"""
113
116
  lt0 = datetime.datetime(t.year, t.month, t.day, hour=0, tzinfo=tz)
114
117
  dst0 = lt0.dst()
@@ -122,28 +125,29 @@ def get_dst_change_hour(t: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> Op
122
125
  return None
123
126
 
124
127
 
125
- def get_dst_change_date_and_row(
126
- d: DateOrDatetime, tz: datetime.tzinfo = Mountain
127
- ) -> Tuple[Optional[datetime.date], Optional[int]]:
128
+ def _get_dst_change_date_and_row(
129
+ d: datetime.date,
130
+ tz: datetime.tzinfo = Mountain,
131
+ ) -> tuple[datetime.date | None, int | None]:
128
132
  """Classify DST information for the WWVB phase modulation signal"""
129
133
  if isdst(d, tz):
130
- n = first_sunday_in_month(d.year, 11)
134
+ n = _first_sunday_in_month(d.year, 11)
131
135
  for offset in range(-28, 28, 7):
132
136
  d1 = n + datetime.timedelta(days=offset)
133
- if is_dst_change_day(d1, tz):
137
+ if _is_dst_change_day(d1, tz):
134
138
  return d1, (offset + 28) // 7
135
139
  else:
136
- m = first_sunday_in_month(d.year + (d.month > 3), 3)
140
+ m = _first_sunday_in_month(d.year + (d.month > 3), 3)
137
141
  for offset in range(0, 52, 7):
138
142
  d1 = m + datetime.timedelta(days=offset)
139
- if is_dst_change_day(d1, tz):
143
+ if _is_dst_change_day(d1, tz):
140
144
  return d1, offset // 7
141
145
 
142
146
  return None, None
143
147
 
144
148
 
145
149
  # "Table 8", likely with transcrption errors
146
- dsttable = [
150
+ _dsttable = [
147
151
  [
148
152
  [
149
153
  0b110001,
@@ -211,19 +215,17 @@ dsttable = [
211
215
  ]
212
216
 
213
217
 
214
- def lfsr_gen(x: List[int]) -> None:
215
- """Generate the 127-bit sequence used in the extended 6-minute codes
216
- except generate 255 bits so that we can simply use any range of [x:x+127]
217
- bits"""
218
+ def _lfsr_gen(x: list[int]) -> None:
219
+ """Generate the next bit of the 6-minute codes sequence"""
218
220
  x.append(x[-7] ^ x[-6] ^ x[-5] ^ x[-2])
219
221
 
220
222
 
221
- lfsr_seq = [1] * 7
222
- while len(lfsr_seq) < 255:
223
- lfsr_gen(lfsr_seq)
223
+ _lfsr_seq = [1] * 7
224
+ while len(_lfsr_seq) < 255:
225
+ _lfsr_gen(_lfsr_seq)
224
226
 
225
227
  # Table 12 - Fixed 106-bit timing word
226
- ftw = [
228
+ _ftw = [
227
229
  int(c)
228
230
  for c in "1101000111"
229
231
  "0101100101"
@@ -239,11 +241,11 @@ ftw = [
239
241
  ]
240
242
 
241
243
 
242
- def get_dst_next(d: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> int:
244
+ def _get_dst_next(d: datetime.date, tz: datetime.tzinfo = Mountain) -> int:
243
245
  """Find the "dst next" value for the phase modulation signal"""
244
246
  dst_now = isdst(d, tz) # dst_on[1]
245
- dst_midwinter = isdst(datetime.datetime(d.year, 1, 1), tz)
246
- dst_midsummer = isdst(datetime.datetime(d.year, 7, 1), tz)
247
+ dst_midwinter = isdst(datetime.datetime(d.year, 1, 1, tzinfo=datetime.timezone.utc), tz)
248
+ dst_midsummer = isdst(datetime.datetime(d.year, 7, 1, tzinfo=datetime.timezone.utc), tz)
247
249
 
248
250
  if dst_midwinter and dst_midsummer:
249
251
  return 0b101111
@@ -254,19 +256,19 @@ def get_dst_next(d: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> int:
254
256
  if dst_midwinter or not dst_midsummer:
255
257
  return 0b100011
256
258
 
257
- dst_change_date, dst_next_row = get_dst_change_date_and_row(d, tz)
259
+ dst_change_date, dst_next_row = _get_dst_change_date_and_row(d, tz)
258
260
  if dst_change_date is None:
259
261
  return 0b100011
260
262
  assert dst_next_row is not None
261
263
 
262
- dst_change_hour = get_dst_change_hour(dst_change_date, tz)
264
+ dst_change_hour = _get_dst_change_hour(dst_change_date, tz)
263
265
  if dst_change_hour is None:
264
266
  return 0b100011
265
267
 
266
- return dsttable[dst_now][dst_change_hour][dst_next_row]
268
+ return _dsttable[dst_now][dst_change_hour][dst_next_row]
267
269
 
268
270
 
269
- hamming_weight = [
271
+ _hamming_weight = [
270
272
  [23, 21, 20, 17, 16, 15, 14, 13, 9, 8, 6, 5, 4, 2, 0],
271
273
  [24, 22, 21, 18, 17, 16, 15, 14, 10, 9, 7, 6, 5, 3, 1],
272
274
  [25, 23, 22, 19, 18, 17, 16, 15, 11, 10, 8, 7, 6, 4, 2],
@@ -281,25 +283,26 @@ SYNC_T = 0x768
281
283
  SYNC_M = 0x1A3A
282
284
 
283
285
 
284
- def extract_bit(v: int, p: int) -> bool:
286
+ def _extract_bit(v: int, p: int) -> bool:
285
287
  """Extract bit 'p' from integer 'v' as a bool"""
286
288
  return bool((v >> p) & 1)
287
289
 
288
290
 
289
- def hamming_parity(value: int) -> int:
291
+ def _hamming_parity(value: int) -> int:
290
292
  """Compute the "hamming parity" of a 26-bit number, such as the minute-of-century
291
293
 
292
- For more details, see Enhanced WWVB Broadcast Format 4.3"""
294
+ For more details, see Enhanced WWVB Broadcast Format 4.3
295
+ """
293
296
  parity = 0
294
297
  for i in range(4, -1, -1):
295
298
  bit = 0
296
- for j in range(0, 15):
297
- bit ^= extract_bit(value, hamming_weight[i][j])
299
+ for j in range(15):
300
+ bit ^= _extract_bit(value, _hamming_weight[i][j])
298
301
  parity = (parity << 1) | bit
299
302
  return parity
300
303
 
301
304
 
302
- dst_ls_lut = [
305
+ _dst_ls_lut = [
303
306
  0b01000,
304
307
  0b10101,
305
308
  0b10110,
@@ -318,35 +321,57 @@ dst_ls_lut = [
318
321
  0b11111,
319
322
  ]
320
323
 
321
- _WWVBMinute = collections.namedtuple("_WWVBMinute", "year days hour min dst ut1 ls ly")
322
324
 
323
-
324
- class WWVBMinute(_WWVBMinute):
325
+ class _WWVBMinute(NamedTuple):
325
326
  """Uniquely identifies a minute of time in the WWVB system.
326
327
 
327
- To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead."""
328
+ To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
329
+ """
328
330
 
329
331
  year: int
332
+ """2-digit year within the WWVB epoch"""
333
+
334
+ days: int
335
+ """1-based day of year"""
336
+
330
337
  hour: int
331
- minute: int
338
+ """UTC hour of day"""
339
+
340
+ min: int
341
+ """Minute of hour"""
342
+
332
343
  dst: int
344
+ """2-bit DST code """
345
+
333
346
  ut1: int
334
- ly: bool
347
+ """UT1 offset in units of 100ms, range -900 to +900ms"""
348
+
335
349
  ls: bool
350
+ """Leap second warning flag"""
351
+
352
+ ly: bool
353
+ """Leap year flag"""
354
+
355
+
356
+ class WWVBMinute(_WWVBMinute):
357
+ """Uniquely identifies a minute of time in the WWVB system.
358
+
359
+ To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
360
+ """
336
361
 
337
362
  epoch: int = 1970
338
363
 
339
- def __new__(
364
+ def __new__( # noqa: PYI034
340
365
  cls,
341
366
  year: int,
342
367
  days: int,
343
368
  hour: int,
344
369
  minute: int,
345
- dst: Optional[int] = None,
346
- ut1: Optional[int] = None,
347
- ls: Optional[bool] = None,
348
- ly: Optional[bool] = None,
349
- ) -> "WWVBMinute":
370
+ dst: int | None = None,
371
+ ut1: int | None = None,
372
+ ls: bool | None = None,
373
+ ly: bool | None = None,
374
+ ) -> WWVBMinute:
350
375
  """Construct a WWVBMinute"""
351
376
  if dst is None:
352
377
  dst = cls.get_dst(year, days)
@@ -383,7 +408,7 @@ class WWVBMinute(_WWVBMinute):
383
408
  @staticmethod
384
409
  def get_dst(year: int, days: int) -> int:
385
410
  """Get the 2-bit WWVB DST value for the given day"""
386
- d0 = datetime.datetime(year, 1, 1) + datetime.timedelta(days - 1)
411
+ d0 = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
387
412
  d1 = d0 + datetime.timedelta(1)
388
413
  dst0 = isdst(d0)
389
414
  dst1 = isdst(d1)
@@ -405,7 +430,12 @@ class WWVBMinute(_WWVBMinute):
405
430
 
406
431
  as_datetime = as_datetime_utc
407
432
 
408
- def as_datetime_local(self, standard_time_offset: int = 7 * 3600, dst_observed: bool = True) -> datetime.datetime:
433
+ def as_datetime_local(
434
+ self,
435
+ standard_time_offset: int = 7 * 3600,
436
+ *,
437
+ dst_observed: bool = True,
438
+ ) -> datetime.datetime:
409
439
  """Convert to a local datetime according to the DST bits"""
410
440
  u = self.as_datetime_utc()
411
441
  offset = datetime.timedelta(seconds=-standard_time_offset)
@@ -426,11 +456,6 @@ class WWVBMinute(_WWVBMinute):
426
456
  offset += datetime.timedelta(seconds=3600)
427
457
  return u.astimezone(datetime.timezone(offset))
428
458
 
429
- def is_ly(self) -> bool:
430
- """Return True if minute is during a leap year"""
431
- warnings.warn("Deprecated, use ly property instead", DeprecationWarning)
432
- return self.ly
433
-
434
459
  def _is_end_of_month(self) -> bool:
435
460
  """Return True if minute is the last minute in a month"""
436
461
  d = self.as_datetime()
@@ -449,18 +474,18 @@ class WWVBMinute(_WWVBMinute):
449
474
  return 59
450
475
  return 61
451
476
 
452
- def as_timecode(self) -> "WWVBTimecode":
477
+ def as_timecode(self) -> WWVBTimecode:
453
478
  """Fill a WWVBTimecode structure representing this minute. Fills both the amplitude and phase codes."""
454
479
  t = WWVBTimecode(self.minute_length())
455
480
 
456
- self.fill_am_timecode(t)
457
- self.fill_pm_timecode(t)
481
+ self._fill_am_timecode(t)
482
+ self._fill_pm_timecode(t)
458
483
 
459
484
  return t
460
485
 
461
486
  @property
462
- def leap_sec(self) -> int:
463
- """Return the 2-bit leap_sec value used by the PM code"""
487
+ def _leap_sec(self) -> int:
488
+ """Return the 2-bit _leap_sec value used by the PM code"""
464
489
  if not self.ls:
465
490
  return 0
466
491
  if self.ut1 < 0:
@@ -477,7 +502,7 @@ class WWVBMinute(_WWVBMinute):
477
502
  // 60
478
503
  )
479
504
 
480
- def fill_am_timecode(self, t: "WWVBTimecode") -> None:
505
+ def _fill_am_timecode(self, t: WWVBTimecode) -> None:
481
506
  """Fill the amplitude (AM) portion of a timecode object"""
482
507
  for i in [0, 9, 19, 29, 39, 49]:
483
508
  t.am[i] = AmplitudeModulation.MARK
@@ -499,7 +524,7 @@ class WWVBMinute(_WWVBMinute):
499
524
  t.am[56] = AmplitudeModulation(self.ls)
500
525
  t._put_am_bcd(self.dst, 57, 58)
501
526
 
502
- def fill_pm_timecode_extended(self, t: "WWVBTimecode") -> None:
527
+ def _fill_pm_timecode_extended(self, t: WWVBTimecode) -> None:
503
528
  """During minutes 10..15 and 40..45, the amplitude signal holds 'extended information'"""
504
529
  assert 10 <= self.min < 16 or 40 <= self.min < 46
505
530
  minno = self.min % 10
@@ -520,103 +545,99 @@ class WWVBMinute(_WWVBMinute):
520
545
  seqno = seqno + 90
521
546
  else:
522
547
  seqno = seqno + 1
523
- else: # dst == 1
524
- if self.hour < 4:
525
- seqno = seqno + 1
526
- elif self.hour < 11:
527
- seqno = seqno + 91
548
+ elif self.hour < 4:
549
+ seqno = seqno + 1
550
+ elif self.hour < 11:
551
+ seqno = seqno + 91
528
552
 
529
- info_seq = lfsr_seq[seqno : seqno + 127]
530
- full_seq = info_seq + ftw + info_seq[::-1]
553
+ info_seq = _lfsr_seq[seqno : seqno + 127]
554
+ full_seq = info_seq + _ftw + info_seq[::-1]
531
555
  assert len(full_seq) == 360
532
556
 
533
557
  offset = minno * 60
534
558
  for i in range(60):
535
559
  t._put_pm_bit(i, full_seq[i + offset])
536
560
 
537
- def fill_pm_timecode_regular(self, t: "WWVBTimecode") -> None:
561
+ def _fill_pm_timecode_regular(self, t: WWVBTimecode) -> None: # noqa: PLR0915
538
562
  """Except during minutes 10..15 and 40..45, the amplitude signal holds 'regular information'"""
539
563
  t._put_pm_bin(0, 13, SYNC_T)
540
564
 
541
565
  moc = self.minute_of_century
542
- leap_sec = self.leap_sec
566
+ _leap_sec = self._leap_sec
543
567
  dst_on = self.dst
544
- dst_ls = dst_ls_lut[dst_on | (leap_sec << 2)]
545
- dst_next = get_dst_next(self.as_datetime())
546
- t._put_pm_bin(13, 5, hamming_parity(moc))
547
- t._put_pm_bit(18, extract_bit(moc, 25))
548
- t._put_pm_bit(19, extract_bit(moc, 0))
549
- t._put_pm_bit(20, extract_bit(moc, 24))
550
- t._put_pm_bit(21, extract_bit(moc, 23))
551
- t._put_pm_bit(22, extract_bit(moc, 22))
552
- t._put_pm_bit(23, extract_bit(moc, 21))
553
- t._put_pm_bit(24, extract_bit(moc, 20))
554
- t._put_pm_bit(25, extract_bit(moc, 19))
555
- t._put_pm_bit(26, extract_bit(moc, 18))
556
- t._put_pm_bit(27, extract_bit(moc, 17))
557
- t._put_pm_bit(28, extract_bit(moc, 16))
558
- t._put_pm_bit(29, False) # Reserved
559
- t._put_pm_bit(30, extract_bit(moc, 15))
560
- t._put_pm_bit(31, extract_bit(moc, 14))
561
- t._put_pm_bit(32, extract_bit(moc, 13))
562
- t._put_pm_bit(33, extract_bit(moc, 12))
563
- t._put_pm_bit(34, extract_bit(moc, 11))
564
- t._put_pm_bit(35, extract_bit(moc, 10))
565
- t._put_pm_bit(36, extract_bit(moc, 9))
566
- t._put_pm_bit(37, extract_bit(moc, 8))
567
- t._put_pm_bit(38, extract_bit(moc, 7))
568
- t._put_pm_bit(39, True) # Reserved
569
- t._put_pm_bit(40, extract_bit(moc, 6))
570
- t._put_pm_bit(41, extract_bit(moc, 5))
571
- t._put_pm_bit(42, extract_bit(moc, 4))
572
- t._put_pm_bit(43, extract_bit(moc, 3))
573
- t._put_pm_bit(44, extract_bit(moc, 2))
574
- t._put_pm_bit(45, extract_bit(moc, 1))
575
- t._put_pm_bit(46, extract_bit(moc, 0))
576
- t._put_pm_bit(47, extract_bit(dst_ls, 4))
577
- t._put_pm_bit(48, extract_bit(dst_ls, 3))
578
- t._put_pm_bit(49, True) # Notice
579
- t._put_pm_bit(50, extract_bit(dst_ls, 2))
580
- t._put_pm_bit(51, extract_bit(dst_ls, 1))
581
- t._put_pm_bit(52, extract_bit(dst_ls, 0))
582
- t._put_pm_bit(53, extract_bit(dst_next, 5))
583
- t._put_pm_bit(54, extract_bit(dst_next, 4))
584
- t._put_pm_bit(55, extract_bit(dst_next, 3))
585
- t._put_pm_bit(56, extract_bit(dst_next, 2))
586
- t._put_pm_bit(57, extract_bit(dst_next, 1))
587
- t._put_pm_bit(58, extract_bit(dst_next, 0))
568
+ dst_ls = _dst_ls_lut[dst_on | (_leap_sec << 2)]
569
+ dst_next = _get_dst_next(self.as_datetime())
570
+ t._put_pm_bin(13, 5, _hamming_parity(moc))
571
+ t._put_pm_bit(18, _extract_bit(moc, 25))
572
+ t._put_pm_bit(19, _extract_bit(moc, 0))
573
+ t._put_pm_bit(20, _extract_bit(moc, 24))
574
+ t._put_pm_bit(21, _extract_bit(moc, 23))
575
+ t._put_pm_bit(22, _extract_bit(moc, 22))
576
+ t._put_pm_bit(23, _extract_bit(moc, 21))
577
+ t._put_pm_bit(24, _extract_bit(moc, 20))
578
+ t._put_pm_bit(25, _extract_bit(moc, 19))
579
+ t._put_pm_bit(26, _extract_bit(moc, 18))
580
+ t._put_pm_bit(27, _extract_bit(moc, 17))
581
+ t._put_pm_bit(28, _extract_bit(moc, 16))
582
+ t._put_pm_bit(29, False) # noqa: FBT003 # Reserved
583
+ t._put_pm_bit(30, _extract_bit(moc, 15))
584
+ t._put_pm_bit(31, _extract_bit(moc, 14))
585
+ t._put_pm_bit(32, _extract_bit(moc, 13))
586
+ t._put_pm_bit(33, _extract_bit(moc, 12))
587
+ t._put_pm_bit(34, _extract_bit(moc, 11))
588
+ t._put_pm_bit(35, _extract_bit(moc, 10))
589
+ t._put_pm_bit(36, _extract_bit(moc, 9))
590
+ t._put_pm_bit(37, _extract_bit(moc, 8))
591
+ t._put_pm_bit(38, _extract_bit(moc, 7))
592
+ t._put_pm_bit(39, True) # noqa: FBT003 # Reserved
593
+ t._put_pm_bit(40, _extract_bit(moc, 6))
594
+ t._put_pm_bit(41, _extract_bit(moc, 5))
595
+ t._put_pm_bit(42, _extract_bit(moc, 4))
596
+ t._put_pm_bit(43, _extract_bit(moc, 3))
597
+ t._put_pm_bit(44, _extract_bit(moc, 2))
598
+ t._put_pm_bit(45, _extract_bit(moc, 1))
599
+ t._put_pm_bit(46, _extract_bit(moc, 0))
600
+ t._put_pm_bit(47, _extract_bit(dst_ls, 4))
601
+ t._put_pm_bit(48, _extract_bit(dst_ls, 3))
602
+ t._put_pm_bit(49, True) # noqa: FBT003 # Notice
603
+ t._put_pm_bit(50, _extract_bit(dst_ls, 2))
604
+ t._put_pm_bit(51, _extract_bit(dst_ls, 1))
605
+ t._put_pm_bit(52, _extract_bit(dst_ls, 0))
606
+ t._put_pm_bit(53, _extract_bit(dst_next, 5))
607
+ t._put_pm_bit(54, _extract_bit(dst_next, 4))
608
+ t._put_pm_bit(55, _extract_bit(dst_next, 3))
609
+ t._put_pm_bit(56, _extract_bit(dst_next, 2))
610
+ t._put_pm_bit(57, _extract_bit(dst_next, 1))
611
+ t._put_pm_bit(58, _extract_bit(dst_next, 0))
588
612
  if len(t.phase) > 59:
589
613
  t._put_pm_bit(59, PhaseModulation.ZERO)
590
614
  if len(t.phase) > 60:
591
615
  t._put_pm_bit(60, PhaseModulation.ZERO)
592
616
 
593
- def fill_pm_timecode(self, t: "WWVBTimecode") -> None:
617
+ def _fill_pm_timecode(self, t: WWVBTimecode) -> None:
594
618
  """Fill the phase portion of a timecode object"""
595
619
  if 10 <= self.min < 16 or 40 <= self.min < 46:
596
- self.fill_pm_timecode_extended(t)
620
+ self._fill_pm_timecode_extended(t)
597
621
  else:
598
- self.fill_pm_timecode_regular(t)
622
+ self._fill_pm_timecode_regular(t)
599
623
 
600
- def next_minute(self, newut1: Optional[int] = None, newls: Optional[bool] = None) -> "WWVBMinute":
624
+ def next_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
601
625
  """Return an object representing the next minute"""
602
626
  d = self.as_datetime() + datetime.timedelta(minutes=1)
603
627
  return self.from_datetime(d, newut1, newls, self)
604
628
 
605
- def previous_minute(self, newut1: Optional[int] = None, newls: Optional[bool] = None) -> "WWVBMinute":
629
+ def previous_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
606
630
  """Return an object representing the previous minute"""
607
631
  d = self.as_datetime() - datetime.timedelta(minutes=1)
608
632
  return self.from_datetime(d, newut1, newls, self)
609
633
 
610
634
  @classmethod
611
- def _get_dut1_info(cls: type, year: int, days: int, old_time: "Optional[WWVBMinute]" = None) -> Tuple[int, bool]:
635
+ def _get_dut1_info(cls: type, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
612
636
  """Return the DUT1 information for a given day, possibly propagating information from a previous timestamp"""
613
637
  if old_time is not None:
614
638
  if old_time.minute_length() != 60:
615
639
  newls = False
616
- if old_time.ut1 < 0:
617
- newut1 = old_time.ut1 + 1000
618
- else:
619
- newut1 = old_time.ut1 - 1000
640
+ newut1 = old_time.ut1 + 1000 if old_time.ut1 < 0 else old_time.ut1 - 1000
620
641
  else:
621
642
  newls = old_time.ls
622
643
  newut1 = old_time.ut1
@@ -624,10 +645,10 @@ class WWVBMinute(_WWVBMinute):
624
645
  return 0, False
625
646
 
626
647
  @classmethod
627
- def fromstring(cls, s: str) -> "WWVBMinute":
648
+ def fromstring(cls, s: str) -> WWVBMinute:
628
649
  """Construct a WWVBMinute from a string representation created by print_timecodes"""
629
650
  s = _removeprefix(s, "WWVB timecode: ")
630
- d: Dict[str, int] = {}
651
+ d: dict[str, int] = {}
631
652
  for part in s.split():
632
653
  k, v = part.split("=")
633
654
  if k == "min":
@@ -637,8 +658,8 @@ class WWVBMinute(_WWVBMinute):
637
658
  days = d.pop("days")
638
659
  hour = d.pop("hour")
639
660
  minute = d.pop("minute")
640
- dst: Optional[int] = d.pop("dst", None)
641
- ut1: Optional[int] = d.pop("ut1", None)
661
+ dst: int | None = d.pop("dst", None)
662
+ ut1: int | None = d.pop("ut1", None)
642
663
  ls = d.pop("ls", None)
643
664
  d.pop("ly", None)
644
665
  if d:
@@ -649,10 +670,10 @@ class WWVBMinute(_WWVBMinute):
649
670
  def from_datetime(
650
671
  cls,
651
672
  d: datetime.datetime,
652
- newut1: Optional[int] = None,
653
- newls: Optional[bool] = None,
654
- old_time: Optional["WWVBMinute"] = None,
655
- ) -> "WWVBMinute":
673
+ newut1: int | None = None,
674
+ newls: bool | None = None,
675
+ old_time: WWVBMinute | None = None,
676
+ ) -> WWVBMinute:
656
677
  """Construct a WWVBMinute from a datetime, possibly specifying ut1/ls data or propagating it from an old time"""
657
678
  u = d.utctimetuple()
658
679
  if newls is None and newut1 is None:
@@ -660,7 +681,7 @@ class WWVBMinute(_WWVBMinute):
660
681
  return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
661
682
 
662
683
  @classmethod
663
- def from_timecode_am(cls, t: "WWVBTimecode") -> Optional["WWVBMinute"]:
684
+ def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None:
664
685
  """Construct a WWVBMinute from a WWVBTimecode"""
665
686
  for i in (0, 9, 19, 29, 39, 49, 59):
666
687
  if t.am[i] != AmplitudeModulation.MARK:
@@ -694,7 +715,7 @@ class WWVBMinute(_WWVBMinute):
694
715
  if days > 366 or (not ly and days > 365):
695
716
  return None
696
717
  ls = bool(t.am[56])
697
- dst = require(t._get_am_bcd(57, 58))
718
+ dst = _require(t._get_am_bcd(57, 58))
698
719
  return cls(year, days, hour, minute, dst, ut1, ls, ly)
699
720
 
700
721
 
@@ -702,12 +723,12 @@ class WWVBMinuteIERS(WWVBMinute):
702
723
  """A WWVBMinute that uses a database of DUT1 information"""
703
724
 
704
725
  @classmethod
705
- def _get_dut1_info(cls, year: int, days: int, old_time: Optional[WWVBMinute] = None) -> Tuple[int, bool]:
706
- d = datetime.datetime(year, 1, 1) + datetime.timedelta(days - 1)
726
+ def _get_dut1_info(cls, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
727
+ d = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
707
728
  return int(round(get_dut1(d) * 10)) * 100, isls(d)
708
729
 
709
730
 
710
- def bcd_bits(n: int) -> Generator[bool, None, None]:
731
+ def _bcd_bits(n: int) -> Generator[bool, None, None]:
711
732
  """Return the bcd representation of n, starting with the least significant bit"""
712
733
  while True:
713
734
  d = n % 10
@@ -738,18 +759,23 @@ class PhaseModulation(enum.IntEnum):
738
759
  class WWVBTimecode:
739
760
  """Represent the amplitude and/or phase signal, usually over 1 minute"""
740
761
 
741
- am: List[AmplitudeModulation]
742
- phase: List[PhaseModulation]
762
+ am: list[AmplitudeModulation]
763
+ """The amplitude modulation data"""
764
+
765
+ phase: list[PhaseModulation]
766
+ """The phase modulation data"""
743
767
 
744
768
  def __init__(self, sz: int) -> None:
769
+ """Construct a WWVB timecode ``sz`` seconds long"""
745
770
  self.am = [AmplitudeModulation.UNSET] * sz
746
771
  self.phase = [PhaseModulation.UNSET] * sz
747
772
 
748
- def _get_am_bcd(self, *poslist: int) -> Optional[int]:
773
+ def _get_am_bcd(self, *poslist: int) -> int | None:
749
774
  """Convert AM data to BCD
750
775
 
751
776
  The the bits ``self.am[poslist[i]]`` in MSB order are converted from
752
- BCD to integer"""
777
+ BCD to integer
778
+ """
753
779
  pos = reversed(poslist)
754
780
  val = [bool(self.am[p]) for p in pos]
755
781
  result = 0
@@ -769,28 +795,29 @@ class WWVBTimecode:
769
795
 
770
796
  The bits at ``self.am[poslist[i]]`` in MSB order are filled with
771
797
  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"""
798
+ Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number
799
+ """
773
800
  pos = list(poslist)[::-1]
774
- for p, b in zip(pos, bcd_bits(v)):
801
+ for p, b in zip(pos, _bcd_bits(v)):
775
802
  if b:
776
803
  self.am[p] = AmplitudeModulation.ONE
777
804
  else:
778
805
  self.am[p] = AmplitudeModulation.ZERO
779
806
 
780
- def _put_pm_bit(self, i: int, v: Union[PhaseModulation, int, bool]) -> None:
807
+ def _put_pm_bit(self, i: int, v: PhaseModulation | int | bool) -> None:
781
808
  """Update a bit of the Phase Modulation signal"""
782
809
  self.phase[i] = PhaseModulation(v)
783
810
 
784
811
  def _put_pm_bin(self, st: int, n: int, v: int) -> None:
785
812
  """Update an n-digit binary number in the Phase Modulation signal"""
786
813
  for i in range(n):
787
- self._put_pm_bit(st + i, extract_bit(v, (n - i - 1)))
814
+ self._put_pm_bit(st + i, _extract_bit(v, (n - i - 1)))
788
815
 
789
816
  def __str__(self) -> str:
790
- """implement str()"""
817
+ """Implement str()"""
791
818
  undefined = [i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET]
792
819
  if undefined:
793
- warnings.warn(f"am{undefined} is unset")
820
+ warnings.warn(f"am{undefined} is unset", stacklevel=1)
794
821
 
795
822
  def convert_one(am: AmplitudeModulation, phase: PhaseModulation) -> str:
796
823
  if phase is PhaseModulation.UNSET:
@@ -802,20 +829,20 @@ class WWVBTimecode:
802
829
  return "".join(convert_one(i, j) for i, j in zip(self.am, self.phase))
803
830
 
804
831
  def __repr__(self) -> str:
805
- """implement repr()"""
832
+ """Implement repr()"""
806
833
  return "<WWVBTimecode " + str(self) + ">"
807
834
 
808
- def to_am_string(self, charset: List[str]) -> str:
835
+ def to_am_string(self, charset: list[str]) -> str:
809
836
  """Convert the amplitude signal to a string"""
810
837
  return "".join(charset[i] for i in self.am)
811
838
 
812
839
  to_string = to_am_string
813
840
 
814
- def to_pm_string(self, charset: List[str]) -> str:
841
+ def to_pm_string(self, charset: list[str]) -> str:
815
842
  """Convert the phase signal to a string"""
816
843
  return "".join(charset[i] for i in self.phase)
817
844
 
818
- def to_both_string(self, charset: List[str]) -> str:
845
+ def to_both_string(self, charset: list[str]) -> str:
819
846
  """Convert both channels to a string"""
820
847
  return "".join(charset[i + j * 3] for i, j in zip(self.am, self.phase))
821
848
 
@@ -847,7 +874,7 @@ def print_timecodes(
847
874
  if first or all_timecodes:
848
875
  if not first:
849
876
  print(file=file)
850
- print(f"WWVB timecode: {str(w)}{channel_text}{style_text}", file=file)
877
+ print(f"WWVB timecode: {w!s}{channel_text}{style_text}", file=file)
851
878
  first = False
852
879
  pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.min:02d} "
853
880
  tc = w.as_timecode()
@@ -872,10 +899,24 @@ def print_timecodes_json(
872
899
  channel: str,
873
900
  file: TextIO,
874
901
  ) -> None:
875
- """Print a range of timecodes with a header. This header is in a format understood by WWVBMinute.fromstring"""
902
+ """Print a range of timecodes in JSON format.
903
+
904
+ The result is a json array of minute data. Each minute data is an object with the following members:
905
+
906
+ * year (int)
907
+ * days (int)
908
+ * hour (int)
909
+ * minute (int)
910
+ * amplitude (string; only if channel is amplitude or both)
911
+ * phase: (string; only if channel is phase or both)
912
+
913
+ The amplitude and phase strings are of length 60 during most minutes, length 61
914
+ during a minute that includes a (positive) leap second, and theoretically
915
+ length 59 in the case of a negative leap second.
916
+ """
876
917
  result = []
877
918
  for _ in range(minutes):
878
- data = {
919
+ data: dict[str, Any] = {
879
920
  "year": w.year,
880
921
  "days": w.days,
881
922
  "hour": w.hour,