wwvb 3.0.7__py3-none-any.whl → 4.0.0a0__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
- T = TypeVar("T") # pylint: disable=invalid-name
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__( # pylint: disable=too-many-arguments
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,107 +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( # pylint: disable=too-many-statements
538
- self, t: "WWVBTimecode"
539
- ) -> None:
561
+ def _fill_pm_timecode_regular(self, t: WWVBTimecode) -> None: # noqa: PLR0915
540
562
  """Except during minutes 10..15 and 40..45, the amplitude signal holds 'regular information'"""
541
563
  t._put_pm_bin(0, 13, SYNC_T)
542
564
 
543
565
  moc = self.minute_of_century
544
- leap_sec = self.leap_sec
566
+ _leap_sec = self._leap_sec
545
567
  dst_on = self.dst
546
- dst_ls = dst_ls_lut[dst_on | (leap_sec << 2)]
547
- dst_next = get_dst_next(self.as_datetime())
548
- t._put_pm_bin(13, 5, hamming_parity(moc))
549
- t._put_pm_bit(18, extract_bit(moc, 25))
550
- t._put_pm_bit(19, extract_bit(moc, 0))
551
- t._put_pm_bit(20, extract_bit(moc, 24))
552
- t._put_pm_bit(21, extract_bit(moc, 23))
553
- t._put_pm_bit(22, extract_bit(moc, 22))
554
- t._put_pm_bit(23, extract_bit(moc, 21))
555
- t._put_pm_bit(24, extract_bit(moc, 20))
556
- t._put_pm_bit(25, extract_bit(moc, 19))
557
- t._put_pm_bit(26, extract_bit(moc, 18))
558
- t._put_pm_bit(27, extract_bit(moc, 17))
559
- t._put_pm_bit(28, extract_bit(moc, 16))
560
- t._put_pm_bit(29, False) # Reserved
561
- t._put_pm_bit(30, extract_bit(moc, 15))
562
- t._put_pm_bit(31, extract_bit(moc, 14))
563
- t._put_pm_bit(32, extract_bit(moc, 13))
564
- t._put_pm_bit(33, extract_bit(moc, 12))
565
- t._put_pm_bit(34, extract_bit(moc, 11))
566
- t._put_pm_bit(35, extract_bit(moc, 10))
567
- t._put_pm_bit(36, extract_bit(moc, 9))
568
- t._put_pm_bit(37, extract_bit(moc, 8))
569
- t._put_pm_bit(38, extract_bit(moc, 7))
570
- t._put_pm_bit(39, True) # Reserved
571
- t._put_pm_bit(40, extract_bit(moc, 6))
572
- t._put_pm_bit(41, extract_bit(moc, 5))
573
- t._put_pm_bit(42, extract_bit(moc, 4))
574
- t._put_pm_bit(43, extract_bit(moc, 3))
575
- t._put_pm_bit(44, extract_bit(moc, 2))
576
- t._put_pm_bit(45, extract_bit(moc, 1))
577
- t._put_pm_bit(46, extract_bit(moc, 0))
578
- t._put_pm_bit(47, extract_bit(dst_ls, 4))
579
- t._put_pm_bit(48, extract_bit(dst_ls, 3))
580
- t._put_pm_bit(49, True) # Notice
581
- t._put_pm_bit(50, extract_bit(dst_ls, 2))
582
- t._put_pm_bit(51, extract_bit(dst_ls, 1))
583
- t._put_pm_bit(52, extract_bit(dst_ls, 0))
584
- t._put_pm_bit(53, extract_bit(dst_next, 5))
585
- t._put_pm_bit(54, extract_bit(dst_next, 4))
586
- t._put_pm_bit(55, extract_bit(dst_next, 3))
587
- t._put_pm_bit(56, extract_bit(dst_next, 2))
588
- t._put_pm_bit(57, extract_bit(dst_next, 1))
589
- 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))
590
612
  if len(t.phase) > 59:
591
613
  t._put_pm_bit(59, PhaseModulation.ZERO)
592
614
  if len(t.phase) > 60:
593
615
  t._put_pm_bit(60, PhaseModulation.ZERO)
594
616
 
595
- def fill_pm_timecode(self, t: "WWVBTimecode") -> None:
617
+ def _fill_pm_timecode(self, t: WWVBTimecode) -> None:
596
618
  """Fill the phase portion of a timecode object"""
597
619
  if 10 <= self.min < 16 or 40 <= self.min < 46:
598
- self.fill_pm_timecode_extended(t)
620
+ self._fill_pm_timecode_extended(t)
599
621
  else:
600
- self.fill_pm_timecode_regular(t)
622
+ self._fill_pm_timecode_regular(t)
601
623
 
602
- 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:
603
625
  """Return an object representing the next minute"""
604
626
  d = self.as_datetime() + datetime.timedelta(minutes=1)
605
627
  return self.from_datetime(d, newut1, newls, self)
606
628
 
607
- 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:
608
630
  """Return an object representing the previous minute"""
609
631
  d = self.as_datetime() - datetime.timedelta(minutes=1)
610
632
  return self.from_datetime(d, newut1, newls, self)
611
633
 
612
634
  @classmethod
613
- def _get_dut1_info( # pylint: disable=unused-argument
614
- cls: type, year: int, days: int, old_time: "Optional[WWVBMinute]" = None
615
- ) -> Tuple[int, bool]:
635
+ def _get_dut1_info(cls: type, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
616
636
  """Return the DUT1 information for a given day, possibly propagating information from a previous timestamp"""
617
637
  if old_time is not None:
618
638
  if old_time.minute_length() != 60:
619
639
  newls = False
620
- if old_time.ut1 < 0:
621
- newut1 = old_time.ut1 + 1000
622
- else:
623
- newut1 = old_time.ut1 - 1000
640
+ newut1 = old_time.ut1 + 1000 if old_time.ut1 < 0 else old_time.ut1 - 1000
624
641
  else:
625
642
  newls = old_time.ls
626
643
  newut1 = old_time.ut1
@@ -628,10 +645,10 @@ class WWVBMinute(_WWVBMinute):
628
645
  return 0, False
629
646
 
630
647
  @classmethod
631
- def fromstring(cls, s: str) -> "WWVBMinute":
648
+ def fromstring(cls, s: str) -> WWVBMinute:
632
649
  """Construct a WWVBMinute from a string representation created by print_timecodes"""
633
650
  s = _removeprefix(s, "WWVB timecode: ")
634
- d: Dict[str, int] = {}
651
+ d: dict[str, int] = {}
635
652
  for part in s.split():
636
653
  k, v = part.split("=")
637
654
  if k == "min":
@@ -641,8 +658,8 @@ class WWVBMinute(_WWVBMinute):
641
658
  days = d.pop("days")
642
659
  hour = d.pop("hour")
643
660
  minute = d.pop("minute")
644
- dst: Optional[int] = d.pop("dst", None)
645
- ut1: Optional[int] = d.pop("ut1", None)
661
+ dst: int | None = d.pop("dst", None)
662
+ ut1: int | None = d.pop("ut1", None)
646
663
  ls = d.pop("ls", None)
647
664
  d.pop("ly", None)
648
665
  if d:
@@ -653,10 +670,10 @@ class WWVBMinute(_WWVBMinute):
653
670
  def from_datetime(
654
671
  cls,
655
672
  d: datetime.datetime,
656
- newut1: Optional[int] = None,
657
- newls: Optional[bool] = None,
658
- old_time: Optional["WWVBMinute"] = None,
659
- ) -> "WWVBMinute":
673
+ newut1: int | None = None,
674
+ newls: bool | None = None,
675
+ old_time: WWVBMinute | None = None,
676
+ ) -> WWVBMinute:
660
677
  """Construct a WWVBMinute from a datetime, possibly specifying ut1/ls data or propagating it from an old time"""
661
678
  u = d.utctimetuple()
662
679
  if newls is None and newut1 is None:
@@ -664,9 +681,7 @@ class WWVBMinute(_WWVBMinute):
664
681
  return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
665
682
 
666
683
  @classmethod
667
- def from_timecode_am( # pylint: disable=too-many-return-statements
668
- cls, t: "WWVBTimecode"
669
- ) -> Optional["WWVBMinute"]:
684
+ def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None:
670
685
  """Construct a WWVBMinute from a WWVBTimecode"""
671
686
  for i in (0, 9, 19, 29, 39, 49, 59):
672
687
  if t.am[i] != AmplitudeModulation.MARK:
@@ -700,7 +715,7 @@ class WWVBMinute(_WWVBMinute):
700
715
  if days > 366 or (not ly and days > 365):
701
716
  return None
702
717
  ls = bool(t.am[56])
703
- dst = require(t._get_am_bcd(57, 58))
718
+ dst = _require(t._get_am_bcd(57, 58))
704
719
  return cls(year, days, hour, minute, dst, ut1, ls, ly)
705
720
 
706
721
 
@@ -708,12 +723,12 @@ class WWVBMinuteIERS(WWVBMinute):
708
723
  """A WWVBMinute that uses a database of DUT1 information"""
709
724
 
710
725
  @classmethod
711
- def _get_dut1_info(cls, year: int, days: int, old_time: Optional[WWVBMinute] = None) -> Tuple[int, bool]:
712
- 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)
713
728
  return int(round(get_dut1(d) * 10)) * 100, isls(d)
714
729
 
715
730
 
716
- def bcd_bits(n: int) -> Generator[bool, None, None]:
731
+ def _bcd_bits(n: int) -> Generator[bool, None, None]:
717
732
  """Return the bcd representation of n, starting with the least significant bit"""
718
733
  while True:
719
734
  d = n % 10
@@ -744,18 +759,23 @@ class PhaseModulation(enum.IntEnum):
744
759
  class WWVBTimecode:
745
760
  """Represent the amplitude and/or phase signal, usually over 1 minute"""
746
761
 
747
- am: List[AmplitudeModulation]
748
- phase: List[PhaseModulation]
762
+ am: list[AmplitudeModulation]
763
+ """The amplitude modulation data"""
764
+
765
+ phase: list[PhaseModulation]
766
+ """The phase modulation data"""
749
767
 
750
768
  def __init__(self, sz: int) -> None:
751
- self.am = [AmplitudeModulation.UNSET] * sz # pylint: disable=invalid-name
769
+ """Construct a WWVB timecode ``sz`` seconds long"""
770
+ self.am = [AmplitudeModulation.UNSET] * sz
752
771
  self.phase = [PhaseModulation.UNSET] * sz
753
772
 
754
- def _get_am_bcd(self, *poslist: int) -> Optional[int]:
773
+ def _get_am_bcd(self, *poslist: int) -> int | None:
755
774
  """Convert AM data to BCD
756
775
 
757
776
  The the bits ``self.am[poslist[i]]`` in MSB order are converted from
758
- BCD to integer"""
777
+ BCD to integer
778
+ """
759
779
  pos = reversed(poslist)
760
780
  val = [bool(self.am[p]) for p in pos]
761
781
  result = 0
@@ -775,28 +795,29 @@ class WWVBTimecode:
775
795
 
776
796
  The bits at ``self.am[poslist[i]]`` in MSB order are filled with
777
797
  the conversion of `v` to BCD
778
- 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
+ """
779
800
  pos = list(poslist)[::-1]
780
- for p, b in zip(pos, bcd_bits(v)):
801
+ for p, b in zip(pos, _bcd_bits(v)):
781
802
  if b:
782
803
  self.am[p] = AmplitudeModulation.ONE
783
804
  else:
784
805
  self.am[p] = AmplitudeModulation.ZERO
785
806
 
786
- 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:
787
808
  """Update a bit of the Phase Modulation signal"""
788
809
  self.phase[i] = PhaseModulation(v)
789
810
 
790
811
  def _put_pm_bin(self, st: int, n: int, v: int) -> None:
791
812
  """Update an n-digit binary number in the Phase Modulation signal"""
792
813
  for i in range(n):
793
- 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)))
794
815
 
795
816
  def __str__(self) -> str:
796
- """implement str()"""
817
+ """Implement str()"""
797
818
  undefined = [i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET]
798
819
  if undefined:
799
- warnings.warn(f"am{undefined} is unset")
820
+ warnings.warn(f"am{undefined} is unset", stacklevel=1)
800
821
 
801
822
  def convert_one(am: AmplitudeModulation, phase: PhaseModulation) -> str:
802
823
  if phase is PhaseModulation.UNSET:
@@ -808,20 +829,20 @@ class WWVBTimecode:
808
829
  return "".join(convert_one(i, j) for i, j in zip(self.am, self.phase))
809
830
 
810
831
  def __repr__(self) -> str:
811
- """implement repr()"""
832
+ """Implement repr()"""
812
833
  return "<WWVBTimecode " + str(self) + ">"
813
834
 
814
- def to_am_string(self, charset: List[str]) -> str:
835
+ def to_am_string(self, charset: list[str]) -> str:
815
836
  """Convert the amplitude signal to a string"""
816
837
  return "".join(charset[i] for i in self.am)
817
838
 
818
839
  to_string = to_am_string
819
840
 
820
- def to_pm_string(self, charset: List[str]) -> str:
841
+ def to_pm_string(self, charset: list[str]) -> str:
821
842
  """Convert the phase signal to a string"""
822
843
  return "".join(charset[i] for i in self.phase)
823
844
 
824
- def to_both_string(self, charset: List[str]) -> str:
845
+ def to_both_string(self, charset: list[str]) -> str:
825
846
  """Convert both channels to a string"""
826
847
  return "".join(charset[i + j * 3] for i, j in zip(self.am, self.phase))
827
848
 
@@ -835,7 +856,6 @@ styles = {
835
856
  }
836
857
 
837
858
 
838
- # pylint: disable=too-many-arguments
839
859
  def print_timecodes(
840
860
  w: WWVBMinute,
841
861
  minutes: int,
@@ -854,7 +874,7 @@ def print_timecodes(
854
874
  if first or all_timecodes:
855
875
  if not first:
856
876
  print(file=file)
857
- 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)
858
878
  first = False
859
879
  pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.min:02d} "
860
880
  tc = w.as_timecode()
@@ -873,17 +893,30 @@ def print_timecodes(
873
893
  w = w.next_minute()
874
894
 
875
895
 
876
- # pylint: disable=too-many-arguments
877
896
  def print_timecodes_json(
878
897
  w: WWVBMinute,
879
898
  minutes: int,
880
899
  channel: str,
881
900
  file: TextIO,
882
901
  ) -> None:
883
- """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
+ """
884
917
  result = []
885
918
  for _ in range(minutes):
886
- data = {
919
+ data: dict[str, Any] = {
887
920
  "year": w.year,
888
921
  "days": w.days,
889
922
  "hour": w.hour,