Add sphinx / rtd documentation

This commit is contained in:
Jeff Epler 2021-10-20 14:53:02 -05:00
parent 8007b02dcc
commit 4fb3e9ea7f
8 changed files with 215 additions and 14 deletions

View file

@ -13,6 +13,20 @@ on:
types: [rerequested]
jobs:
docs:
runs-on: ubuntu-20.04
steps:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install deps
run: python -mpip install -r requirements-dev.txt
- name: Build HTML docs
run: make html
test:
strategy:
matrix:

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: CC0-1.0
/build
/_build
/.coverage
/dist
/*.egg-info

View file

@ -13,6 +13,30 @@ coverage:
.PHONY: mypy
mypy:
mypy --strict *.py
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?= -a -E -j auto
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
.PHONY: help
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Route particular targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
.PHONY: html
html:
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Copyright (C) 2021 Jeff Epler <jepler@gmail.com>
# SPDX-FileCopyrightText: 2021 Jeff Epler
#

0
_static/.empty Normal file
View file

65
conf.py Normal file
View file

@ -0,0 +1,65 @@
# pylint: disable=all
# fmt: off
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'leapseconddata'
copyright = '2021, Jeff Epler'
author = 'Jeff Epler'
# The full version, including alpha/beta/rc tags
release = '1.1.0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
autodoc_typehints = "description"
autodoc_class_signature = "separated"
# SPDX-FileCopyrightText: 2021 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only

26
index.rst Normal file
View file

@ -0,0 +1,26 @@
.. SPDX-FileCopyrightText: 2021 Jeff Epler
..
.. SPDX-License-Identifier: GPL-3.0-only
leapseconddata
==============
.. image:: https://github.com/jepler/leapseconddata/actions/workflows/test.yml/badge.svg
:target: https://github.com/jepler/leapseconddata/actions/workflows/test.yml
:alt: Test leapseconddata
.. image:: https://codecov.io/gh/jepler/leapseconddata/branch/main/graph/badge.svg?token=Exx0c3Gp65
:target: https://codecov.io/gh/jepler/leapseconddata
:alt: codecov
.. image:: https://img.shields.io/pypi/v/leapseconddata
:target: https://pypi.org/project/leapseconddata
:alt: PyPI
.. toctree::
:maxdepth: 2
:caption: Contents:
.. automodule:: leapseconddata
:members:

View file

@ -4,7 +4,21 @@
#
# SPDX-License-Identifier: GPL-3.0-only
"""Use the list of known and scheduled leap seconds"""
"""Use the list of known and scheduled leap seconds
For example, to retrieve the UTC-TAI offset on January 1, 2011:
.. code-block:: python
:emphasize-lines: 2,3,5
>>> import datetime
>>> import leapseconddata
>>> ls = leapseconddata.LeapSecondData.from_standard_source()
>>> when = datetime.datetime(2011, 1, 1, tzinfo=datetime.timezone.utc)
>>> ls.tai_offset(when).total_seconds()
34.0
"""
import datetime
import hashlib
@ -60,10 +74,24 @@ well in the past. Use `valid_until` to determine validity."""
class LeapSecondData(_LeapSecondData):
"""Represent the list of known and scheduled leapseconds"""
"""Represent the list of known and scheduled leapseconds
:param List[LeapSecondInfo] leap_seconds: A list of leap seconds
:param Optional[datetime.datetime] valid_until: The expiration of the data, if available
:param Optional[datetime.datetime] updated: The last update time of the data, if available
"""
__slots__ = ()
leap_seconds: List[LeapSecondInfo]
"""All known and scheduled leap seconds"""
valid_until: Optional[datetime.datetime]
"""The list is valid until this UTC time"""
last_updated: Optional[datetime.datetime]
"""The last time the list was updated to add a new upcoming leap second"""
def __new__(
cls,
leap_seconds: List[LeapSecondInfo],
@ -82,7 +110,10 @@ class LeapSecondData(_LeapSecondData):
return None
def valid(self, when: Optional[datetime.datetime] = None) -> bool:
"""Return True if the data is valid at given datetime (or the current moment, if None is passed)"""
"""Return True if the data is valid at given datetime (or the current moment, if None is passed)
:param when: Moment to check for validity
"""
return self._check_validity(when) is None
@staticmethod
@ -96,10 +127,14 @@ class LeapSecondData(_LeapSecondData):
) -> datetime.timedelta:
"""For a given datetime, return the TAI-UTC offset
:param when: Moment in time to find offset for
:param check_validity: Check whether the database is valid for the given moment
For times before the first leap second, a zero offset is returned.
For times after the end of the file's validity, an exception is raised
unless `check_validity=False` is passed. In this case, it will return
the offset of last list entry."""
the offset of last list entry.
"""
is_tai = when.tzinfo is tai
if not is_tai:
@ -124,7 +159,12 @@ class LeapSecondData(_LeapSecondData):
def to_tai(
self, when: datetime.datetime, check_validity: bool = True
) -> datetime.datetime:
"""Convert the given datetime object to TAI"""
"""Convert the given datetime object to TAI.
:param when: Moment in time to convert. If naive, it is assumed to be in UTC.
:param check_validity: Check whether the database is valid for the given moment
Naive timestamps are assumed to be utc. A TAI timestamp is returned unchanged."""
if when.tzinfo is tai:
return when
when = self._utc_datetime(when)
@ -133,7 +173,11 @@ class LeapSecondData(_LeapSecondData):
def tai_to_utc(
self, when: datetime.datetime, check_validity: bool = True
) -> datetime.datetime:
"""Convert the given datetime object (which is assumed to be in TAI) to UTC"""
"""Convert the given datetime object to UTC
:param when: Moment in time to convert. If naive, its ``tzinfo`` must be `tai`.
:param check_validity: Check whether the database is valid for the given moment
"""
if when.tzinfo is not None and when.tzinfo is not tai:
raise ValueError("Input timestamp is not TAI or naive")
if when.tzinfo is None:
@ -148,6 +192,9 @@ class LeapSecondData(_LeapSecondData):
) -> bool:
"""Return True if the given timestamp is the leap second.
:param when: Moment in time to check. If naive, it is assumed to be in UTC.
:param check_validity: Check whether the database is valid for the given moment
For a TAI timestamp, it returns True for the leap second (the one that
would be shown as :60 in UTC). For a UTC timestamp, it returns True
for the :59 second, since the :60 second cannot be represented."""
@ -161,9 +208,16 @@ class LeapSecondData(_LeapSecondData):
@classmethod
def from_standard_source(
cls, when: Optional[datetime.datetime] = None
cls,
when: Optional[datetime.datetime] = None,
check_hash: bool = True,
) -> "LeapSecondData":
"""Using a list of standard sources, including network sources, find a
"""Get the list of leap seconds from a standard source.
:param when: Check that the data is valid for this moment
:param check_hash: Whether to check the embedded hash
Using a list of standard sources, including network sources, find a
leap-second.list data valid for the given timestamp, or the current
time (if unspecified)"""
@ -174,7 +228,7 @@ class LeapSecondData(_LeapSecondData):
]:
logging.debug("Trying leap second data from %s", location)
try:
candidate = cls.from_url(location)
candidate = cls.from_url(location, check_hash)
except InvalidHashError: # pragma no cover
logging.warning("Invalid hash while reading %s", location)
continue
@ -195,8 +249,10 @@ class LeapSecondData(_LeapSecondData):
) -> "LeapSecondData":
"""Retrieve the leap second list from a local file.
The default location is the standard location for the file on
Debian systems."""
:param filename: Local filename to read leap second data from. The
default is the standard location for the file on Debian systems.
:param check_hash: Whether to check the embedded hash
"""
with open(filename, "rb") as open_file: # pragma no cover
return cls.from_open_file(open_file, check_hash)
@ -208,7 +264,10 @@ class LeapSecondData(_LeapSecondData):
) -> "LeapSecondData":
"""Retrieve the leap second list from a local file
The default location is the official copy of the data from IETF"""
:param filename: URL to read leap second data from. The
default is maintained by the IETF
:param check_hash: Whether to check the embedded hash
"""
with urllib.request.urlopen(url) as open_file:
return cls.from_open_file(open_file, check_hash)
@ -218,7 +277,12 @@ class LeapSecondData(_LeapSecondData):
data: Union[bytes, str],
check_hash: bool = True,
) -> "LeapSecondData":
"""Retrieve the leap second list from local data"""
"""Retrieve the leap second list from local data
:param filename: URL to read leap second data from. The
default is maintained by the IETF
:param check_hash: Whether to check the embedded hash
"""
if isinstance(data, str):
data = data.encode("ascii", "replace")
return cls.from_open_file(io.BytesIO(data), check_hash)
@ -236,7 +300,11 @@ class LeapSecondData(_LeapSecondData):
def from_open_file(
cls, open_file: BinaryIO, check_hash: bool = True
) -> "LeapSecondData":
"""Retrieve the leap second list from an open file-like object"""
"""Retrieve the leap second list from an open file-like object
:param filename: Readable file containing the leap second data
:param check_hash: Whether to check the embedded hash
"""
leap_seconds: List[LeapSecondInfo] = []
valid_until = None
last_updated = None

View file

@ -6,5 +6,8 @@ coverage
mypy; implementation_name=="cpython"
pre-commit
setuptools>=45
sphinx
sphinx-autodoc-typehints
sphinx-rtd-theme
twine
wheel