wwvb 4.0.0a0__py3-none-any.whl → 4.1.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/testwwvb.py DELETED
@@ -1,403 +0,0 @@
1
- #!/usr/bin/python3
2
- # ruff: noqa: E501
3
-
4
- """Test most wwvblib functionality"""
5
-
6
- # Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
7
- # SPDX-FileCopyrightText: 2021 Jeff Epler
8
- #
9
- # SPDX-License-Identifier: GPL-3.0-only
10
-
11
- from __future__ import annotations
12
-
13
- import copy
14
- import datetime
15
- import io
16
- import pathlib
17
- import random
18
- import sys
19
- import unittest
20
-
21
- import uwwvb
22
-
23
- import wwvb
24
-
25
- from . import decode, iersdata, tz
26
-
27
-
28
- class WWVBMinute2k(wwvb.WWVBMinute):
29
- """Treats the origin of the 2-digit epoch as 2000"""
30
-
31
- epoch = 2000
32
-
33
-
34
- class WWVBTestCase(unittest.TestCase):
35
- """Test each expected output in tests/. Some outputs are from another program, some are from us"""
36
-
37
- maxDiff = 131072
38
-
39
- def test_cases(self) -> None:
40
- """Generate a test case for each expected output in tests/"""
41
- for test in pathlib.Path("tests").glob("*"):
42
- with self.subTest(test=test):
43
- text = test.read_text(encoding="utf-8")
44
- lines = [line for line in text.split("\n") if not line.startswith("#")]
45
- while not lines[0]:
46
- del lines[0]
47
- text = "\n".join(lines)
48
- header = lines[0].split()
49
- timestamp = " ".join(header[:10])
50
- options = header[10:]
51
- channel = "amplitude"
52
- style = "default"
53
- for o in options:
54
- if o.startswith("--channel="):
55
- channel = o[10:]
56
- elif o.startswith("--style="):
57
- style = o[8:]
58
- else:
59
- raise ValueError(f"Unknown option {o!r}")
60
- num_minutes = len(lines) - 2
61
- if channel == "both":
62
- num_minutes = len(lines) // 3
63
-
64
- num_headers = sum(line.startswith("WWVB timecode") for line in lines)
65
- if num_headers > 1:
66
- all_timecodes = True
67
- num_minutes = num_headers
68
- else:
69
- all_timecodes = False
70
-
71
- w = wwvb.WWVBMinute.fromstring(timestamp)
72
- result = io.StringIO()
73
- wwvb.print_timecodes(
74
- w,
75
- num_minutes,
76
- channel=channel,
77
- style=style,
78
- all_timecodes=all_timecodes,
79
- file=result,
80
- )
81
- result_str = result.getvalue()
82
- self.assertEqual(text, result_str)
83
-
84
-
85
- class WWVBRoundtrip(unittest.TestCase):
86
- """Round-trip tests"""
87
-
88
- def test_decode(self) -> None:
89
- """Test that a range of minutes including a leap second are correctly decoded by the state-based decoder"""
90
- minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
91
- decoder = decode.wwvbreceive()
92
- next(decoder)
93
- decoder.send(wwvb.AmplitudeModulation.MARK)
94
- any_leap_second = False
95
- for _ in range(20):
96
- timecode = minute.as_timecode()
97
- decoded: wwvb.WWVBTimecode | None = None
98
- if len(timecode.am) == 61:
99
- any_leap_second = True
100
- for code in timecode.am:
101
- decoded = decoder.send(code) or decoded
102
- assert decoded
103
- self.assertEqual(
104
- timecode.am[:60],
105
- decoded.am,
106
- f"Checking equality of minute {minute}: [expected] {timecode.am} != [actual] {decoded.am}",
107
- )
108
- minute = minute.next_minute()
109
- self.assertTrue(any_leap_second)
110
-
111
- def test_cover_fill_pm_timecode_extended(self) -> None:
112
- """Get full coverage of the function pm_timecode_extended"""
113
- for dt in (
114
- datetime.datetime(1992, 1, 1, tzinfo=datetime.timezone.utc),
115
- datetime.datetime(1992, 4, 5, tzinfo=datetime.timezone.utc),
116
- datetime.datetime(1992, 6, 1, tzinfo=datetime.timezone.utc),
117
- datetime.datetime(1992, 10, 25, tzinfo=datetime.timezone.utc),
118
- ):
119
- for hour in (0, 4, 11):
120
- dt1 = dt.replace(hour=hour, minute=10)
121
- minute = wwvb.WWVBMinuteIERS.from_datetime(dt1)
122
- assert minute is not None
123
- timecode = minute.as_timecode().am
124
- assert timecode
125
-
126
- def test_roundtrip(self) -> None:
127
- """Test that a wide of minutes are correctly decoded by the state-based decoder"""
128
- dt = datetime.datetime(1992, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
129
- delta = datetime.timedelta(minutes=915 if sys.implementation.name == "cpython" else 86400 - 915)
130
- while dt.year < 1993:
131
- minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
132
- assert minute is not None
133
- timecode = minute.as_timecode().am
134
- assert timecode
135
- decoded_minute: wwvb.WWVBMinute | None = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode())
136
- assert decoded_minute
137
- decoded = decoded_minute.as_timecode().am
138
- self.assertEqual(
139
- timecode,
140
- decoded,
141
- f"Checking equality of minute {minute}: [expected] {timecode} != [actual] {decoded}",
142
- )
143
- dt = dt + delta
144
-
145
- def test_noise(self) -> None:
146
- """Test against pseudorandom noise"""
147
- minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
148
- r = random.Random(408)
149
- junk = [
150
- r.choice(
151
- [
152
- wwvb.AmplitudeModulation.MARK,
153
- wwvb.AmplitudeModulation.ONE,
154
- wwvb.AmplitudeModulation.ZERO,
155
- ],
156
- )
157
- for _ in range(480)
158
- ]
159
- timecode = minute.as_timecode()
160
- test_input = [*junk, wwvb.AmplitudeModulation.MARK, *timecode.am]
161
- decoder = decode.wwvbreceive()
162
- next(decoder)
163
- for code in test_input[:-1]:
164
- decoded = decoder.send(code)
165
- self.assertIsNone(decoded)
166
- decoded = decoder.send(wwvb.AmplitudeModulation.MARK)
167
- assert decoded
168
- self.assertIsNotNone(decoded)
169
- self.assertEqual(
170
- timecode.am[:60],
171
- decoded.am,
172
- f"Checking equality of minute {minute}: [expected] {timecode.am} != [actual] {decoded.am}",
173
- )
174
-
175
- def test_noise2(self) -> None:
176
- """Test of the full minute decoder with targeted errors to get full coverage"""
177
- minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
178
- timecode = minute.as_timecode()
179
- decoded = wwvb.WWVBMinute.from_timecode_am(timecode)
180
- self.assertIsNotNone(decoded)
181
- for position in uwwvb.always_mark:
182
- test_input = copy.deepcopy(timecode)
183
- for noise in (0, 1):
184
- test_input.am[position] = wwvb.AmplitudeModulation(noise)
185
- decoded = wwvb.WWVBMinute.from_timecode_am(test_input)
186
- self.assertIsNone(decoded)
187
- for position in uwwvb.always_zero:
188
- test_input = copy.deepcopy(timecode)
189
- for noise in (1, 2):
190
- test_input.am[position] = wwvb.AmplitudeModulation(noise)
191
- decoded = wwvb.WWVBMinute.from_timecode_am(test_input)
192
- self.assertIsNone(decoded)
193
- for i in range(8):
194
- if i in (0b101, 0b010): # Test the 6 impossible bit-combos
195
- continue
196
- test_input = copy.deepcopy(timecode)
197
- test_input.am[36] = wwvb.AmplitudeModulation(i & 1)
198
- test_input.am[37] = wwvb.AmplitudeModulation((i >> 1) & 1)
199
- test_input.am[38] = wwvb.AmplitudeModulation((i >> 2) & 1)
200
- decoded = wwvb.WWVBMinute.from_timecode_am(test_input)
201
- self.assertIsNone(decoded)
202
- # Invalid year-day
203
- test_input = copy.deepcopy(timecode)
204
- test_input.am[22] = wwvb.AmplitudeModulation(1)
205
- test_input.am[23] = wwvb.AmplitudeModulation(1)
206
- test_input.am[25] = wwvb.AmplitudeModulation(1)
207
- decoded = wwvb.WWVBMinute.from_timecode_am(test_input)
208
- self.assertIsNone(decoded)
209
-
210
- def test_noise3(self) -> None:
211
- """Test impossible BCD values"""
212
- minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
213
- timecode = minute.as_timecode()
214
-
215
- for poslist in [
216
- [1, 2, 3, 4], # tens minutes
217
- [5, 6, 7, 8], # ones minutes
218
- [15, 16, 17, 18], # tens hours
219
- [25, 26, 27, 28], # tens days
220
- [30, 31, 32, 33], # ones days
221
- [40, 41, 42, 43], # tens years
222
- [45, 46, 47, 48], # ones years
223
- [50, 51, 52, 53], # ones dut1
224
- ]:
225
- with self.subTest(test=poslist):
226
- test_input = copy.deepcopy(timecode)
227
- for pi in poslist:
228
- test_input.am[pi] = wwvb.AmplitudeModulation(1)
229
- decoded = wwvb.WWVBMinute.from_timecode_am(test_input)
230
- self.assertIsNone(decoded)
231
-
232
- def test_previous_next_minute(self) -> None:
233
- """Test that previous minute and next minute are inverses"""
234
- minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
235
- self.assertEqual(minute, minute.next_minute().previous_minute())
236
-
237
- def test_timecode_str(self) -> None:
238
- """Test the str() and repr() methods"""
239
- minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
240
- timecode = minute.as_timecode()
241
- self.assertEqual(
242
- str(timecode),
243
- "₂₁⁰¹⁰₀⁰⁰₀²₀₀₁₀₀⁰₀¹¹₂₀⁰⁰¹₀₁⁰⁰₀₂₀⁰₁⁰₀₀⁰₁⁰²⁰¹¹₀⁰¹₀⁰¹²⁰⁰¹₀₀¹₁₁₁₂",
244
- )
245
- timecode.phase = [wwvb.PhaseModulation.UNSET] * 60
246
- self.assertEqual(
247
- repr(timecode),
248
- "<WWVBTimecode 210100000200100001120001010002001000010201100100120010011112>",
249
- )
250
-
251
- def test_extreme_dut1(self) -> None:
252
- """Test extreme dut1 dates"""
253
- s = iersdata.DUT1_DATA_START
254
- sm1 = s - datetime.timedelta(days=1)
255
- self.assertEqual(wwvb.get_dut1(s), wwvb.get_dut1(sm1))
256
-
257
- e = iersdata.DUT1_DATA_START + datetime.timedelta(days=len(iersdata.DUT1_OFFSETS) - 1)
258
- ep1 = e + datetime.timedelta(days=1)
259
-
260
- self.assertEqual(wwvb.get_dut1(e), wwvb.get_dut1(ep1))
261
-
262
- ep2 = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=340)
263
- wwvb.get_dut1(ep2)
264
-
265
- def test_epoch(self) -> None:
266
- """Test the 1970-to-2069 epoch"""
267
- m = wwvb.WWVBMinute(69, 1, 1, 0, 0)
268
- n = wwvb.WWVBMinute(2069, 1, 1, 0, 0)
269
- self.assertEqual(m, n)
270
-
271
- m = wwvb.WWVBMinute(70, 1, 1, 0, 0)
272
- n = wwvb.WWVBMinute(1970, 1, 1, 0, 0)
273
- self.assertEqual(m, n)
274
-
275
- def test_fromstring(self) -> None:
276
- """Test the fromstring() classmethod"""
277
- s = "WWVB timecode: year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
278
- t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
279
- self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
280
- t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ls=1"
281
- self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
282
- t = "year=1998 days=365 hour=23 min=56 dst=0"
283
- self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
284
-
285
- def test_from_datetime(self) -> None:
286
- """Test the from_datetime() classmethod"""
287
- d = datetime.datetime(1998, 12, 31, 23, 56, 0, tzinfo=datetime.timezone.utc)
288
- self.assertEqual(
289
- wwvb.WWVBMinuteIERS.from_datetime(d),
290
- wwvb.WWVBMinuteIERS.from_datetime(d, newls=True, newut1=-300),
291
- )
292
-
293
- def test_exceptions(self) -> None:
294
- """Test some error detection"""
295
- with self.assertRaises(ValueError):
296
- wwvb.WWVBMinute(2021, 1, 1, 1, dst=4)
297
-
298
- with self.assertRaises(ValueError):
299
- wwvb.WWVBMinute(2021, 1, 1, 1, ut1=1)
300
-
301
- with self.assertRaises(ValueError):
302
- wwvb.WWVBMinute(2021, 1, 1, 1, ls=False)
303
-
304
- with self.assertRaises(ValueError):
305
- wwvb.WWVBMinute.fromstring("year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1")
306
-
307
- def test_update(self) -> None:
308
- """Ensure that the 'maybe_warn_update' function is covered"""
309
- with self.assertWarnsRegex(Warning, "updateiers"):
310
- wwvb._maybe_warn_update(datetime.date(1970, 1, 1))
311
- wwvb._maybe_warn_update(datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc))
312
-
313
- def test_undefined(self) -> None:
314
- """Ensure that the check for unset elements in am works"""
315
- with self.assertWarnsRegex(Warning, "is unset"):
316
- str(wwvb.WWVBTimecode(60))
317
-
318
- def test_tz(self) -> None:
319
- """Get a little more coverage in the dst change functions"""
320
- date, row = wwvb._get_dst_change_date_and_row(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc))
321
- self.assertIsNone(date)
322
- self.assertIsNone(row)
323
-
324
- self.assertIsNone(wwvb._get_dst_change_hour(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)))
325
-
326
- self.assertEqual(wwvb._get_dst_next(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)), 0b000111)
327
-
328
- # Cuba followed year-round DST for several years
329
- self.assertEqual(
330
- wwvb._get_dst_next(datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc), tz=tz.ZoneInfo("Cuba")),
331
- 0b101111,
332
- )
333
- date, row = wwvb._get_dst_change_date_and_row(
334
- datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc),
335
- tz=tz.ZoneInfo("Cuba"),
336
- )
337
- self.assertIsNone(date)
338
- self.assertIsNone(row)
339
-
340
- # California was weird in 1948
341
- self.assertEqual(
342
- wwvb._get_dst_next(
343
- datetime.datetime(1948, 1, 1, tzinfo=datetime.timezone.utc),
344
- tz=tz.ZoneInfo("America/Los_Angeles"),
345
- ),
346
- 0b100011,
347
- )
348
-
349
- # Berlin had DST changes on Monday in 1917
350
- self.assertEqual(
351
- wwvb._get_dst_next(
352
- datetime.datetime(1917, 1, 1, tzinfo=datetime.timezone.utc),
353
- tz=tz.ZoneInfo("Europe/Berlin"),
354
- ),
355
- 0b100011,
356
- )
357
-
358
- #
359
- # Australia observes DST in the other half of the year compared to the
360
- # Northern hemisphere
361
- self.assertEqual(
362
- wwvb._get_dst_next(
363
- datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc),
364
- tz=tz.ZoneInfo("Australia/Melbourne"),
365
- ),
366
- 0b100011,
367
- )
368
-
369
- def test_epoch2(self) -> None:
370
- """Test that the settable epoch feature works"""
371
- self.assertEqual(wwvb.WWVBMinute(0, 1, 1, 0, 0).year, 2000)
372
- self.assertEqual(wwvb.WWVBMinute(69, 1, 1, 0, 0).year, 2069)
373
- self.assertEqual(wwvb.WWVBMinute(70, 1, 1, 0, 0).year, 1970)
374
- self.assertEqual(wwvb.WWVBMinute(99, 1, 1, 0, 0).year, 1999)
375
-
376
- # 4-digit years can always be used
377
- self.assertEqual(wwvb.WWVBMinute(2000, 1, 1, 0, 0).year, 2000)
378
- self.assertEqual(wwvb.WWVBMinute(2069, 1, 1, 0, 0).year, 2069)
379
- self.assertEqual(wwvb.WWVBMinute(1970, 1, 1, 0, 0).year, 1970)
380
- self.assertEqual(wwvb.WWVBMinute(1999, 1, 1, 0, 0).year, 1999)
381
-
382
- self.assertEqual(wwvb.WWVBMinute(1900, 1, 1, 0, 0).year, 1900)
383
- self.assertEqual(wwvb.WWVBMinute(1969, 1, 1, 0, 0).year, 1969)
384
- self.assertEqual(wwvb.WWVBMinute(2070, 1, 1, 0, 0).year, 2070)
385
- self.assertEqual(wwvb.WWVBMinute(2099, 1, 1, 0, 0).year, 2099)
386
-
387
- self.assertEqual(WWVBMinute2k(0, 1, 1, 0, 0).year, 2000)
388
- self.assertEqual(WWVBMinute2k(99, 1, 1, 0, 0).year, 2099)
389
-
390
- # 4-digit years can always be used
391
- self.assertEqual(WWVBMinute2k(2000, 1, 1, 0, 0).year, 2000)
392
- self.assertEqual(WWVBMinute2k(2069, 1, 1, 0, 0).year, 2069)
393
- self.assertEqual(WWVBMinute2k(1970, 1, 1, 0, 0).year, 1970)
394
- self.assertEqual(WWVBMinute2k(1999, 1, 1, 0, 0).year, 1999)
395
-
396
- self.assertEqual(WWVBMinute2k(1900, 1, 1, 0, 0).year, 1900)
397
- self.assertEqual(WWVBMinute2k(1969, 1, 1, 0, 0).year, 1969)
398
- self.assertEqual(WWVBMinute2k(2070, 1, 1, 0, 0).year, 2070)
399
- self.assertEqual(WWVBMinute2k(2099, 1, 1, 0, 0).year, 2099)
400
-
401
-
402
- if __name__ == "__main__": # pragma no cover
403
- unittest.main()
wwvb/tz.py DELETED
@@ -1,13 +0,0 @@
1
- # -*- python -*-
2
- """A library for WWVB timecodes"""
3
-
4
- # Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
5
- # SPDX-FileCopyrightText: 2021 Jeff Epler
6
- #
7
- # SPDX-License-Identifier: GPL-3.0-only
8
-
9
- from zoneinfo import ZoneInfo
10
-
11
- Mountain = ZoneInfo("America/Denver")
12
-
13
- __all__ = ["Mountain", "ZoneInfo"]
wwvb/updateiers.py DELETED
@@ -1,199 +0,0 @@
1
- #!/usr/bin/python3
2
-
3
- # SPDX-FileCopyrightText: 2021 Jeff Epler
4
- #
5
- # SPDX-License-Identifier: GPL-3.0-only
6
-
7
- """Update the DUT1 and LS data based on online sources"""
8
-
9
- from __future__ import annotations
10
-
11
- import csv
12
- import datetime
13
- import io
14
- import itertools
15
- import pathlib
16
- from typing import Callable
17
-
18
- import bs4
19
- import click
20
- import platformdirs
21
- import requests
22
-
23
- DIST_PATH = pathlib.Path(__file__).parent / "iersdata_dist.py"
24
-
25
- OLD_TABLE_START: datetime.date | None = None
26
- OLD_TABLE_END: datetime.date | None = None
27
- if DIST_PATH.exists():
28
- import wwvb.iersdata_dist
29
-
30
- OLD_TABLE_START = wwvb.iersdata_dist.DUT1_DATA_START
31
- OLD_TABLE_END = OLD_TABLE_START + datetime.timedelta(days=len(wwvb.iersdata_dist.DUT1_OFFSETS) - 1)
32
-
33
- IERS_URL = "https://datacenter.iers.org/data/csv/finals2000A.all.csv"
34
- IERS_PATH = pathlib.Path("finals2000A.all.csv")
35
- if IERS_PATH.exists():
36
- IERS_URL = str(IERS_PATH)
37
- print("using local", IERS_URL)
38
- NIST_URL = "https://www.nist.gov/pml/time-and-frequency-division/atomic-standards/leap-second-and-ut1-utc-information"
39
-
40
-
41
- def _get_text(url: str) -> str:
42
- """Get a local file or a http/https URL"""
43
- if url.startswith("http"):
44
- with requests.get(url, timeout=30) as response:
45
- return response.text
46
- else:
47
- return pathlib.Path(url).read_text(encoding="utf-8")
48
-
49
-
50
- def update_iersdata( # noqa: PLR0915, PLR0912
51
- target_path: pathlib.Path,
52
- ) -> None:
53
- """Update iersdata.py"""
54
- offsets: list[int] = []
55
- iersdata_text = _get_text(IERS_URL)
56
- for r in csv.DictReader(io.StringIO(iersdata_text), delimiter=";"):
57
- jd = float(r["MJD"])
58
- offs_str = r["UT1-UTC"]
59
- if not offs_str:
60
- break
61
- offs = int(round(float(offs_str) * 10))
62
- if not offsets:
63
- table_start = datetime.date(1858, 11, 17) + datetime.timedelta(jd)
64
-
65
- when = min(datetime.date(1972, 1, 1), table_start)
66
- # iers bulletin A doesn't cover 1972, so fake data for those
67
- # leap seconds
68
- while when < datetime.date(1972, 7, 1):
69
- offsets.append(-2)
70
- when = when + datetime.timedelta(days=1)
71
- while when < datetime.date(1972, 11, 1):
72
- offsets.append(8)
73
- when = when + datetime.timedelta(days=1)
74
- while when < datetime.date(1972, 12, 1):
75
- offsets.append(0)
76
- when = when + datetime.timedelta(days=1)
77
- while when < datetime.date(1973, 1, 1):
78
- offsets.append(-2)
79
- when = when + datetime.timedelta(days=1)
80
- while when < table_start:
81
- offsets.append(8)
82
- when = when + datetime.timedelta(days=1)
83
-
84
- table_start = min(datetime.date(1972, 1, 1), table_start)
85
-
86
- offsets.append(offs)
87
-
88
- wwvb_text = _get_text(NIST_URL)
89
- wwvb_data = bs4.BeautifulSoup(wwvb_text, features="html.parser")
90
- wwvb_dut1_table = wwvb_data.findAll("table")[2]
91
- assert wwvb_dut1_table
92
- meta = wwvb_data.find("meta", property="article:modified_time")
93
- assert isinstance(meta, bs4.Tag)
94
- wwvb_data_stamp = datetime.datetime.fromisoformat(meta.attrs["content"]).replace(tzinfo=None).date()
95
-
96
- def patch(patch_start: datetime.date, patch_end: datetime.date, val: int) -> None:
97
- off_start = (patch_start - table_start).days
98
- off_end = (patch_end - table_start).days
99
- offsets[off_start:off_end] = [val] * (off_end - off_start)
100
-
101
- wwvb_dut1: int | None = None
102
- wwvb_start: datetime.date | None = None
103
- for row in wwvb_dut1_table.findAll("tr")[1:][::-1]:
104
- cells = row.findAll("td")
105
- when = datetime.datetime.strptime(cells[0].text + "+0000", "%Y-%m-%d%z").date()
106
- dut1 = cells[2].text.replace("s", "").replace(" ", "")
107
- dut1 = int(round(float(dut1) * 10))
108
- if wwvb_dut1 is not None:
109
- assert wwvb_start is not None
110
- patch(wwvb_start, when, wwvb_dut1)
111
- wwvb_dut1 = dut1
112
- wwvb_start = when
113
-
114
- # As of 2021-06-14, NIST website incorrectly indicates the offset of -600ms
115
- # persisted through 2009-03-12, causing an incorrect leap second inference.
116
- # Assume instead that NIST started broadcasting +400ms on January 1, 2009,
117
- # causing the leap second to occur on 2008-12-31.
118
- patch(datetime.date(2009, 1, 1), datetime.date(2009, 3, 12), 4)
119
-
120
- # this is the final (most recent) wwvb DUT1 value broadcast. We want to
121
- # extend it some distance into the future, but how far? We will use the
122
- # modified timestamp of the NIST data.
123
- assert wwvb_dut1 is not None
124
- assert wwvb_start is not None
125
- patch(wwvb_start, wwvb_data_stamp + datetime.timedelta(days=1), wwvb_dut1)
126
-
127
- with target_path.open("w", encoding="utf-8") as output:
128
-
129
- def code(*args: str) -> None:
130
- """Print to the output file"""
131
- print(*args, file=output)
132
-
133
- code("# -*- python3 -*-")
134
- code("# fmt: off")
135
- code('"""File generated from public data - not subject to copyright"""')
136
- code("# SPDX" + "-FileCopyrightText: Public domain")
137
- code("# SPDX" + "-License-Identifier: CC0-1.0")
138
- code("# isort: skip_file")
139
- code("import datetime")
140
-
141
- code("__all__ = ['DUT1_DATA_START', 'DUT1_OFFSETS']")
142
- code(f"DUT1_DATA_START = {table_start!r}")
143
- c = sorted(chr(ord("a") + ch + 10) for ch in set(offsets))
144
- code(f"{','.join(c)} = tuple({''.join(c)!r})")
145
- code(f"DUT1_OFFSETS = str( # {table_start.year:04d}{table_start.month:02d}{table_start.day:02d}")
146
- line = ""
147
- j = 0
148
-
149
- for val, it in itertools.groupby(offsets):
150
- part = ""
151
- ch = chr(ord("a") + val + 10)
152
- sz = len(list(it))
153
- if j:
154
- part = part + "+"
155
- part = part + ch if sz < 2 else part + f"{ch}*{sz}"
156
- j += sz
157
- if len(line + part) > 60:
158
- d = table_start + datetime.timedelta(j - 1)
159
- code(f" {line:<60s} # {d.year:04d}{d.month:02d}{d.day:02d}")
160
- line = part
161
- else:
162
- line = line + part
163
- d = table_start + datetime.timedelta(j - 1)
164
- code(f" {line:<60s} # {d.year:04d}{d.month:02d}{d.day:02d}")
165
- code(")")
166
- table_end = table_start + datetime.timedelta(len(offsets) - 1)
167
- if OLD_TABLE_START:
168
- print(f"old iersdata covered {OLD_TABLE_START} .. {OLD_TABLE_END}")
169
- print(f"iersdata covers {table_start} .. {table_end}")
170
-
171
-
172
- def iersdata_path(callback: Callable[[str, str], pathlib.Path]) -> pathlib.Path:
173
- """Find out the path for this directory"""
174
- print("iersdata_path", callback)
175
- r = callback("wwvbpy", "unpythonic.net") / "wwvb_iersdata.py"
176
- print(f"iersdata_path {r=!r}")
177
- return r
178
-
179
-
180
- @click.command()
181
- @click.option(
182
- "--user",
183
- "location",
184
- flag_value=iersdata_path(platformdirs.user_data_path),
185
- default=iersdata_path(platformdirs.user_data_path),
186
- type=pathlib.Path,
187
- )
188
- @click.option("--dist", "location", flag_value=DIST_PATH)
189
- @click.option("--site", "location", flag_value=iersdata_path(platformdirs.site_data_path))
190
- def main(location: str) -> None:
191
- """Update DUT1 data"""
192
- path = pathlib.Path(location)
193
- print(f"will write to {location!r}")
194
- path.parent.mkdir(parents=True, exist_ok=True)
195
- update_iersdata(path)
196
-
197
-
198
- if __name__ == "__main__":
199
- main()