wwvb 4.0.0__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.
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # SPDX-FileCopyrightText: 2021 Jeff Epler
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-only
6
+
7
+ """Use the list of known and scheduled leap seconds
8
+
9
+ For example, to retrieve the UTC-TAI offset on January 1, 2011:
10
+
11
+ .. code-block:: python
12
+ :emphasize-lines: 2,3,5
13
+
14
+ >>> import datetime
15
+ >>> import leapseconddata
16
+ >>> ls = leapseconddata.LeapSecondData.from_standard_source()
17
+ >>> when = datetime.datetime(2011, 1, 1, tzinfo=datetime.timezone.utc)
18
+ >>> ls.tai_offset(when).total_seconds()
19
+ 34.0
20
+
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import datetime
26
+ import hashlib
27
+ import io
28
+ import logging
29
+ import pathlib
30
+ import re
31
+ import urllib.request
32
+ from dataclasses import dataclass, field
33
+ from typing import BinaryIO
34
+
35
+ tai = datetime.timezone(datetime.timedelta(0), "TAI")
36
+
37
+ NTP_EPOCH = datetime.datetime(1900, 1, 1, tzinfo=datetime.timezone.utc)
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class LeapSecondInfo:
42
+ """Information about a particular leap second"""
43
+
44
+ start: datetime.datetime
45
+ """The UTC timestamp just after the insertion of the leap second.
46
+
47
+ The leap second is actually the 61th second of the previous minute (xx:xx:60)"""
48
+
49
+ tai_offset: datetime.timedelta
50
+ """The new TAI-UTC offset. Positive numbers indicate that TAI is ahead of UTC"""
51
+
52
+
53
+ class ValidityError(ValueError):
54
+ """The leap second information is not valid for the given timestamp"""
55
+
56
+
57
+ class InvalidHashError(ValueError):
58
+ """The file hash could not be verified"""
59
+
60
+
61
+ def _from_ntp_epoch(value: int) -> datetime.datetime:
62
+ return NTP_EPOCH + datetime.timedelta(seconds=value)
63
+
64
+
65
+ def datetime_is_tai(when: datetime.datetime) -> bool:
66
+ """Return true if the datetime is in the TAI timescale"""
67
+ return when.tzname() == "TAI"
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class LeapSecondData:
72
+ """Represent the list of known and scheduled leapseconds
73
+
74
+ :param List[LeapSecondInfo] leap_seconds: A list of leap seconds
75
+ :param Optional[datetime.datetime] valid_until: The expiration of the data
76
+ :param Optional[datetime.datetime] updated: The last update time of the data
77
+ """
78
+
79
+ leap_seconds: list[LeapSecondInfo]
80
+ """All known and scheduled leap seconds"""
81
+
82
+ valid_until: datetime.datetime | None = field(default=None)
83
+ """The list is valid until this UTC time"""
84
+
85
+ last_updated: datetime.datetime | None = field(default=None)
86
+ """The last time the list was updated to add a new upcoming leap second"""
87
+
88
+ def _check_validity(self, when: datetime.datetime | None) -> str | None:
89
+ if when is None:
90
+ when = datetime.datetime.now(datetime.timezone.utc)
91
+ if not self.valid_until:
92
+ return "Data validity unknown"
93
+ if when > self.valid_until:
94
+ return f"Data only valid until {self.valid_until:%Y-%m-%d}"
95
+ return None
96
+
97
+ def valid(self, when: datetime.datetime | None = None) -> bool:
98
+ """Return True if the data is valid at given datetime
99
+
100
+ If `when` is none, the validity for the current moment is checked.
101
+
102
+ :param when: Moment to check for validity
103
+ """
104
+ return self._check_validity(when) is None
105
+
106
+ @staticmethod
107
+ def _utc_datetime(when: datetime.datetime) -> datetime.datetime:
108
+ if when.tzinfo is not None and when.tzinfo is not datetime.timezone.utc:
109
+ when = when.astimezone(datetime.timezone.utc)
110
+ return when
111
+
112
+ def tai_offset(self, when: datetime.datetime, *, check_validity: bool = True) -> datetime.timedelta:
113
+ """For a given datetime, return the TAI-UTC offset
114
+
115
+ :param when: Moment in time to find offset for
116
+ :param check_validity: Check whether the database is valid for the given moment
117
+
118
+ For times before the first leap second, a zero offset is returned.
119
+ For times after the end of the file's validity, an exception is raised
120
+ unless `check_validity=False` is passed. In this case, it will return
121
+ the offset of the last list entry.
122
+ """
123
+ is_tai = datetime_is_tai(when)
124
+ if not is_tai:
125
+ when = self._utc_datetime(when)
126
+ if check_validity:
127
+ message = self._check_validity(when)
128
+ if message is not None:
129
+ raise ValidityError(message)
130
+
131
+ if not self.leap_seconds:
132
+ return datetime.timedelta(0)
133
+
134
+ old_tai = datetime.timedelta()
135
+ for leap_second in self.leap_seconds:
136
+ start = leap_second.start
137
+ if is_tai:
138
+ start += leap_second.tai_offset - datetime.timedelta(seconds=1)
139
+ if when < start:
140
+ return old_tai
141
+ old_tai = leap_second.tai_offset
142
+ return self.leap_seconds[-1].tai_offset
143
+
144
+ def to_tai(self, when: datetime.datetime, *, check_validity: bool = True) -> datetime.datetime:
145
+ """Convert the given datetime object to TAI.
146
+
147
+ :param when: Moment in time to convert. If naive, it is assumed to be in UTC.
148
+ :param check_validity: Check whether the database is valid for the given moment
149
+
150
+ Naive timestamps are assumed to be UTC. A TAI timestamp is returned unchanged.
151
+ """
152
+ if datetime_is_tai(when):
153
+ return when
154
+ when = self._utc_datetime(when)
155
+ return (when + self.tai_offset(when, check_validity=check_validity)).replace(tzinfo=tai)
156
+
157
+ def tai_to_utc(self, when: datetime.datetime, *, check_validity: bool = True) -> datetime.datetime:
158
+ """Convert the given datetime object to UTC
159
+
160
+ For a leap second, the ``fold`` property of the returned time is True.
161
+
162
+ :param when: Moment in time to convert. If not naive, its ``tzinfo`` must be `tai`.
163
+ :param check_validity: Check whether the database is valid for the given moment
164
+ """
165
+ if when.tzinfo is not None and when.tzinfo is not tai:
166
+ raise ValueError("Input timestamp is not TAI or naive")
167
+ if when.tzinfo is None:
168
+ when = when.replace(tzinfo=tai)
169
+ result = (when - self.tai_offset(when, check_validity=check_validity)).replace(tzinfo=datetime.timezone.utc)
170
+ if self.is_leap_second(when, check_validity=check_validity):
171
+ result = result.replace(fold=True)
172
+ return result
173
+
174
+ def is_leap_second(self, when: datetime.datetime, *, check_validity: bool = True) -> bool:
175
+ """Return True if the given timestamp is the leap second.
176
+
177
+ :param when: Moment in time to check. If naive, it is assumed to be in UTC.
178
+ :param check_validity: Check whether the database is valid for the given moment
179
+
180
+ For a TAI timestamp, it returns True for the leap second (the one that
181
+ would be shown as :60 in UTC). For a UTC timestamp, it returns True
182
+ for the :59 second if ``fold``, since the :60 second cannot be
183
+ represented.
184
+ """
185
+ if when.tzinfo is not tai:
186
+ when = self.to_tai(when, check_validity=check_validity) + datetime.timedelta(seconds=when.fold)
187
+ tai_offset1 = self.tai_offset(when, check_validity=check_validity)
188
+ tai_offset2 = self.tai_offset(when - datetime.timedelta(seconds=1), check_validity=check_validity)
189
+ return tai_offset1 != tai_offset2
190
+
191
+ @classmethod
192
+ def from_standard_source(
193
+ cls,
194
+ when: datetime.datetime | None = None,
195
+ *,
196
+ check_hash: bool = True,
197
+ ) -> LeapSecondData:
198
+ """Get the list of leap seconds from a standard source.
199
+
200
+ :param when: Check that the data is valid for this moment
201
+ :param check_hash: Whether to check the embedded hash
202
+
203
+ Using a list of standard sources, including network sources, find a
204
+ leap-second.list data valid for the given timestamp, or the current
205
+ time (if unspecified)
206
+ """
207
+ for location in [ # pragma no branch
208
+ "file:///usr/share/zoneinfo/leap-seconds.list", # Debian Linux
209
+ "file:///var/db/ntpd.leap-seconds.list", # FreeBSD
210
+ "https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list",
211
+ "https://www.meinberg.de/download/ntp/leap-seconds.list",
212
+ ]:
213
+ logging.debug("Trying leap second data from %s", location)
214
+ try:
215
+ candidate = cls.from_url(location, check_hash=check_hash)
216
+ except InvalidHashError: # pragma no cover
217
+ logging.warning("Invalid hash while reading %s", location)
218
+ continue
219
+ if candidate is None: # pragma no cover
220
+ continue
221
+ if candidate.valid(when): # pragma no branch
222
+ logging.info("Using leap second data from %s", location)
223
+ return candidate
224
+ logging.warning("Validity expired for %s", location) # pragma no cover
225
+
226
+ raise ValidityError("No valid leap-second.list file could be found") # pragma no cover
227
+
228
+ @classmethod
229
+ def from_file(
230
+ cls,
231
+ filename: str = "/usr/share/zoneinfo/leap-seconds.list",
232
+ *,
233
+ check_hash: bool = True,
234
+ ) -> LeapSecondData:
235
+ """Retrieve the leap second list from a local file.
236
+
237
+ :param filename: Local filename to read leap second data from. The
238
+ default is the standard location for the file on Debian systems.
239
+ :param check_hash: Whether to check the embedded hash
240
+ """
241
+ with pathlib.Path(filename).open("rb") as open_file: # pragma no cover
242
+ return cls.from_open_file(open_file, check_hash=check_hash)
243
+
244
+ @classmethod
245
+ def from_url(
246
+ cls,
247
+ url: str = "https://raw.githubusercontent.com/eggert/tz/main/leap-seconds.list",
248
+ *,
249
+ check_hash: bool = True,
250
+ ) -> LeapSecondData | None:
251
+ """Retrieve the leap second list from a local file
252
+
253
+ :param filename: URL to read leap second data from. The
254
+ default is maintained by the tzdata authors
255
+ :param check_hash: Whether to check the embedded hash
256
+ """
257
+ try:
258
+ with urllib.request.urlopen(url) as open_file:
259
+ return cls.from_open_file(open_file, check_hash=check_hash)
260
+ except urllib.error.URLError: # pragma no cover
261
+ return None
262
+
263
+ @classmethod
264
+ def from_data(
265
+ cls,
266
+ data: bytes | str,
267
+ *,
268
+ check_hash: bool = True,
269
+ ) -> LeapSecondData:
270
+ """Retrieve the leap second list from local data
271
+
272
+ :param data: Data to parse as a leap second list
273
+ :param check_hash: Whether to check the embedded hash
274
+ """
275
+ if isinstance(data, str):
276
+ data = data.encode("ascii", "replace")
277
+ return cls.from_open_file(io.BytesIO(data), check_hash=check_hash)
278
+
279
+ @staticmethod
280
+ def _parse_content_hash(row: bytes) -> str:
281
+ """Transform the SHA1 content into a string that matches `hexdigest()`
282
+
283
+ The SHA1 hash of the leap second content is stored in an unusual way.
284
+ """
285
+ parts = row.split()
286
+ hash_parts = [int(s, 16) for s in parts[1:]]
287
+ return "".join(f"{i:08x}" for i in hash_parts)
288
+
289
+ @classmethod
290
+ def from_open_file(cls, open_file: BinaryIO, *, check_hash: bool = True) -> LeapSecondData:
291
+ """Retrieve the leap second list from an open file-like object
292
+
293
+ :param open_file: Binary IO object containing the leap second list
294
+ :param check_hash: Whether to check the embedded hash
295
+ """
296
+ leap_seconds: list[LeapSecondInfo] = []
297
+ valid_until = None
298
+ last_updated = None
299
+ content_to_hash = []
300
+ content_hash = None
301
+
302
+ hasher = hashlib.sha1()
303
+
304
+ for row in open_file:
305
+ row = row.strip() # noqa: PLW2901
306
+ if row.startswith(b"#h"):
307
+ content_hash = cls._parse_content_hash(row)
308
+ continue
309
+
310
+ if row.startswith(b"#@"):
311
+ parts = row.split()
312
+ hasher.update(parts[1])
313
+ valid_until = _from_ntp_epoch(int(parts[1]))
314
+ continue
315
+
316
+ if row.startswith(b"#$"):
317
+ parts = row.split()
318
+ hasher.update(parts[1])
319
+ last_updated = _from_ntp_epoch(int(parts[1]))
320
+ continue
321
+
322
+ row = row.split(b"#")[0].strip() # noqa: PLW2901
323
+ content_to_hash.extend(re.findall(rb"\d+", row))
324
+
325
+ parts = row.split()
326
+ if len(parts) != 2: # noqa: PLR2004
327
+ continue
328
+ hasher.update(parts[0])
329
+ hasher.update(parts[1])
330
+
331
+ when = _from_ntp_epoch(int(parts[0]))
332
+ tai_offset = datetime.timedelta(seconds=int(parts[1]))
333
+ leap_seconds.append(LeapSecondInfo(when, tai_offset))
334
+
335
+ if check_hash:
336
+ if content_hash is None:
337
+ raise InvalidHashError("No #h line found")
338
+ digest = hasher.hexdigest()
339
+ if digest != content_hash:
340
+ raise InvalidHashError(f"Hash didn't match. Expected {content_hash[:8]}..., got {digest[:8]}...")
341
+
342
+ return LeapSecondData(leap_seconds, valid_until, last_updated)
@@ -0,0 +1,169 @@
1
+ # SPDX-FileCopyrightText: 2022 Jeff Epler
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+
5
+ """Commandline interface to leap second data"""
6
+
7
+ from __future__ import annotations
8
+
9
+ import datetime
10
+ import logging
11
+ import typing
12
+ from dataclasses import dataclass
13
+
14
+ import click
15
+
16
+ from . import LeapSecondData, tai
17
+
18
+ utc = datetime.timezone.utc
19
+
20
+
21
+ def utcnow() -> datetime.datetime:
22
+ """Return the current time in UTC, with tzinfo=utc"""
23
+ return datetime.datetime.now(utc)
24
+
25
+
26
+ class UTCDateTime(click.DateTime):
27
+ """Click option class for date time in UTC"""
28
+
29
+ def convert(
30
+ self,
31
+ value: typing.Any,
32
+ param: click.Parameter | None,
33
+ ctx: click.Context | None,
34
+ ) -> typing.Any:
35
+ """Convert the value then attach the utc timezone"""
36
+ converted = super().convert(value, param, ctx)
37
+ if converted is not None:
38
+ return converted.replace(tzinfo=utc)
39
+ return converted # pragma no cover
40
+
41
+
42
+ @dataclass
43
+ class State:
44
+ """State shared across sub-commands"""
45
+
46
+ leap_second_data: LeapSecondData
47
+
48
+
49
+ @click.group()
50
+ @click.option(
51
+ "--url",
52
+ type=str,
53
+ default=None,
54
+ help="URL for leap second data (unspecified to use default source)",
55
+ )
56
+ @click.option("--debug/--no-debug", type=bool)
57
+ @click.pass_context
58
+ def cli(ctx: click.Context, *, url: str, debug: bool) -> None:
59
+ """Access leap second database information"""
60
+ if debug: # pragma no cover
61
+ logging.getLogger().setLevel(logging.DEBUG)
62
+ if ctx.find_object(LeapSecondData) is None: # pragma no branch
63
+ if url is None:
64
+ ctx.obj = LeapSecondData.from_standard_source()
65
+ else: # pragma no cover
66
+ ctx.obj = LeapSecondData.from_url(url)
67
+
68
+
69
+ @cli.command()
70
+ @click.pass_context
71
+ def info(ctx: click.Context) -> None:
72
+ """Show information about leap second database"""
73
+ leap_second_data = ctx.obj
74
+ print(f"Last updated: {leap_second_data.last_updated:%Y-%m-%d}")
75
+ print(f"Valid until: {leap_second_data.valid_until:%Y-%m-%d}")
76
+ # The first leap_seconds entry
77
+ print(f"{len(leap_second_data.leap_seconds)-1} leap seconds")
78
+
79
+
80
+ @cli.command()
81
+ @click.pass_context
82
+ @click.option("--tai/--utc", "is_tai", default=False)
83
+ @click.argument("timestamp", type=UTCDateTime(), default=utcnow(), metavar="TIMESTAMP")
84
+ def offset(ctx: click.Context, *, is_tai: bool, timestamp: datetime.datetime) -> None:
85
+ """Get the UTC offset for a given moment, in seconds"""
86
+ leap_second_data = ctx.obj
87
+ if is_tai:
88
+ timestamp = timestamp.replace(tzinfo=tai)
89
+ print(f"{leap_second_data.tai_offset(timestamp).total_seconds():.0f}")
90
+
91
+
92
+ @cli.command()
93
+ @click.pass_context
94
+ @click.option("--to-tai/--to-utc", default=True)
95
+ @click.argument("timestamp", type=UTCDateTime(), default=None, required=False, metavar="TIMESTAMP")
96
+ def convert(ctx: click.Context, *, to_tai: bool, timestamp: datetime.datetime | None = None) -> None:
97
+ """Convert timestamps between TAI and UTC"""
98
+ leap_second_data = ctx.obj
99
+ if to_tai:
100
+ if timestamp is None:
101
+ timestamp = utcnow()
102
+ when_tai = leap_second_data.to_tai(timestamp)
103
+ print(f"{when_tai:%Y-%m-%d %H:%M:%S} TAI")
104
+ else:
105
+ if timestamp is None: # pragma no cover
106
+ raise click.UsageError("--to-utc requires explicit timestamp", ctx)
107
+ when_utc = leap_second_data.tai_to_utc(timestamp.replace(tzinfo=tai))
108
+ if when_utc.fold:
109
+ print(f"{when_utc:%Y-%m-%d %H:%M:60} UTC")
110
+ else:
111
+ print(f"{when_utc:%Y-%m-%d %H:%M:%S} UTC")
112
+
113
+
114
+ @cli.command()
115
+ @click.pass_context
116
+ @click.argument("timestamp", type=UTCDateTime(), default=utcnow(), metavar="TIMESTAMP")
117
+ def next_leapsecond(ctx: click.Context, *, timestamp: datetime.datetime) -> None:
118
+ """Get the next leap second after a given UTC timestamp"""
119
+ leap_second_data = ctx.obj
120
+ ls = min(
121
+ (ls for ls in leap_second_data.leap_seconds if ls.start > timestamp),
122
+ default=None,
123
+ key=lambda x: x.start,
124
+ )
125
+ if ls is None:
126
+ print("None")
127
+ else:
128
+ print(f"{ls.start:%Y-%m-%d %H:%M:%S} UTC")
129
+
130
+
131
+ @cli.command()
132
+ @click.pass_context
133
+ @click.argument("timestamp", type=UTCDateTime(), default=utcnow(), metavar="TIMESTAMP")
134
+ def previous_leapsecond(ctx: click.Context, *, timestamp: datetime.datetime) -> None:
135
+ """Get the last leap second before a given UTC timestamp"""
136
+ leap_second_data = ctx.obj
137
+ ls = max(
138
+ (ls for ls in leap_second_data.leap_seconds if ls.start < timestamp),
139
+ default=None,
140
+ key=lambda x: x.start,
141
+ )
142
+ if ls is None:
143
+ print("None")
144
+ else:
145
+ print(f"{ls.start:%Y-%m-%d %H:%M:%S} UTC")
146
+
147
+
148
+ @cli.command()
149
+ @click.argument(
150
+ "start",
151
+ type=UTCDateTime(),
152
+ default=datetime.datetime(1972, 1, 1, tzinfo=utc),
153
+ metavar="START-TIMESTAMP",
154
+ )
155
+ @click.argument("end", type=UTCDateTime(), default=utcnow(), metavar="[END-TIMESTAMP]")
156
+ @click.pass_context
157
+ def table(ctx: click.Context, *, start: datetime.datetime, end: datetime.datetime) -> None:
158
+ """Print information about leap seconds"""
159
+ leap_second_data = ctx.obj
160
+ for leap_second in leap_second_data.leap_seconds: # pragma no branch
161
+ if leap_second.start < start:
162
+ continue
163
+ if leap_second.start > end:
164
+ break
165
+ print(f"{leap_second.start:%Y-%m-%d}: {leap_second.tai_offset.total_seconds():.0f}")
166
+
167
+
168
+ if __name__ == "__main__": # pragma no cover
169
+ cli()
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '4.0.0'
16
- __version_tuple__ = version_tuple = (4, 0, 0)
15
+ __version__ = version = '4.1.0a0'
16
+ __version_tuple__ = version_tuple = (4, 1, 0)
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.1
2
+ Name: wwvb
3
+ Version: 4.1.0a0
4
+ Summary: Use the list of known and scheduled leap seconds
5
+ Author-email: Jeff Epler <jepler@gmail.com>
6
+ Project-URL: Source, https://github.com/jepler/leapseconddata
7
+ Project-URL: Documentation, https://leapseconddata.readthedocs.io/en/latest/
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: click
19
+
20
+ <!--
21
+ SPDX-FileCopyrightText: 2021 Jeff Epler
22
+
23
+ SPDX-License-Identifier: GPL-3.0-only
24
+ -->
25
+ [![Test leapseconddata](https://github.com/jepler/leapseconddata/actions/workflows/test.yml/badge.svg)](https://github.com/jepler/leapseconddata/actions/workflows/test.yml)
26
+ [![PyPI](https://img.shields.io/pypi/v/leapseconddata)](https://pypi.org/project/leapseconddata)
27
+ [![Documentation Status](https://readthedocs.org/projects/leapseconddata/badge/?version=latest)](https://leapseconddata.readthedocs.io/en/latest/?badge=latest)
28
+
29
+ # Python Leap Second List
30
+
31
+ Leap seconds are corrections applied irregularly so that the UTC day stays
32
+ fixed to the earth's rotation.
33
+
34
+ This module provides a class for parsing and validating the standard
35
+ `leap-seconds.list` file. Once parsed, it is possible to retrieve the
36
+ full list of leap seconds, or find the TAI-UTC offset for any UTC time.
37
+
38
+ # `leapsecond` program
39
+
40
+ Access leap second data from the command line.
41
+
42
+ ```
43
+ Usage: leapsecond [OPTIONS] COMMAND [ARGS]...
44
+
45
+ Access leap second database information
46
+
47
+ Options:
48
+ --url TEXT URL for leap second data (unspecified to use default
49
+ source)
50
+ --debug / --no-debug
51
+ --help Show this message and exit.
52
+
53
+ Commands:
54
+ convert Convert timestamps between TAI and UTC
55
+ info Show information about leap second database
56
+ next-leapsecond Get the next leap second after a given UTC timestamp
57
+ offset Get the UTC offset for a given moment, in seconds
58
+ previous-leapsecond Get the last leap second before a given UTC timestamp
59
+ table Print information about leap seconds
60
+ ```
@@ -0,0 +1,9 @@
1
+ leapseconddata/__init__.py,sha256=UyHCvvcJjbIugNEEQDf45HmhzFiIe77fNVGRMm7eNdg,13017
2
+ leapseconddata/__main__.py,sha256=c26T1gFoVwh1goK0_mRCQ3-lAsz8eCFYf8aFi0nESf8,5405
3
+ leapseconddata/__version__.py,sha256=XeG8WsGuH3HJ5DBhMc289w1eiYW9-G0-4h58Y3MV650,413
4
+ leapseconddata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ wwvb-4.1.0a0.dist-info/METADATA,sha256=KmLslzML53yHVtNANGMbfuL4t4Xu5OvevkIC85tnWEw,2496
6
+ wwvb-4.1.0a0.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
7
+ wwvb-4.1.0a0.dist-info/entry_points.txt,sha256=Ca5FvuhALDcLhQrO8H7l91Bg0hjkxkzLEwLU-9qY9dk,59
8
+ wwvb-4.1.0a0.dist-info/top_level.txt,sha256=82tQRfAH4hrno1b9AR-UhFUHrbBxENUJ2aUtEH-ctTs,15
9
+ wwvb-4.1.0a0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ leapsecond = leapseconddata.__main__:cli
@@ -0,0 +1 @@
1
+ leapseconddata