Add more complete documentation

This commit is contained in:
Thomas Touhey 2024-07-18 21:41:49 +02:00
parent 2fad09fac6
commit f982c49db7
24 changed files with 460 additions and 10 deletions

2
.gitignore vendored
View file

@ -12,3 +12,5 @@ __pycache__
*,cover *,cover
/.reuse /.reuse
__version__.py __version__.py
/docs/_build
/.idea

View file

@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: 2024 Thomas Touhey
# SPDX-License-Identifier: GPL-3.0-only
"""Sphinx extension to remove the first line from module docstrings."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from sphinx.application import Sphinx
def remove_first_line_in_module_docstring(
_app: Sphinx,
what: str,
_name: str,
_obj: Any,
_options: Any,
lines: list[str],
) -> None:
"""Remove the first line from the docstring.
This is because the first line of the docstring is summed up in the
document title, before the module autodoc.
"""
if what != "module" or not lines:
return
for i in range(1, len(lines)):
if not lines[i]:
lines[: i + 1] = []
return
lines[:] = []
def setup(app: Sphinx) -> None:
"""Set up the extension."""
app.connect(
"autodoc-process-docstring",
remove_first_line_in_module_docstring,
)

13
docs/code.rst Normal file
View file

@ -0,0 +1,13 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
Code reference
==============
If you are looking for information on a specific function, class or method,
this part of the documentation is for you.
.. toctree::
:maxdepth: 1
code/leapseconddata

View file

@ -0,0 +1,7 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
``leapseconddata`` -- main namespace for the project
====================================================
.. automodule:: leapseconddata

View file

@ -17,6 +17,7 @@ import sys
import pathlib import pathlib
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
sys.path.append(str(pathlib.Path(__file__).parent / "_ext"))
# Define the canonical URL if you are using a custom domain on Read the Docs # Define the canonical URL if you are using a custom domain on Read the Docs
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "")
@ -29,7 +30,7 @@ if os.environ.get("READTHEDOCS", "") == "True":
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'leapseconddata' project = 'leapseconddata'
copyright = '2021, Jeff Epler' copyright = '2021-2024, Jeff Epler'
author = 'Jeff Epler' author = 'Jeff Epler'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
@ -43,6 +44,9 @@ release = '1.1.0'
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'custom_autodoc',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@ -51,7 +55,10 @@ templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path. # This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '_env'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '_env', '.license']
# Show the contents of todo directives.
todo_include_todos = True
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
@ -66,8 +73,22 @@ html_theme = 'sphinx_rtd_theme'
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ['_static']
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
}
autodoc_typehints = "description" autodoc_typehints = "description"
autodoc_typehints_format = "short"
autodoc_class_signature = "separated" autodoc_class_signature = "separated"
autodoc_default_options = {
"members": True,
"undoc-members": True,
"inherited-members": False,
"special-members": False,
"exclude-members": "__init__",
"show-inheritance": True,
}
autodoc_member_order = "bysource"
# SPDX-FileCopyrightText: 2021 Jeff Epler # SPDX-FileCopyrightText: 2021 Jeff Epler
# #

16
docs/developer-guides.rst Normal file
View file

@ -0,0 +1,16 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
Developer guides
================
This section consists of multiple guides for solving specific problems,
targeted towards developers using the component.
.. toctree::
:maxdepth: 2
developer-guides/obtaining-leap-seconds
developer-guides/converting-tai-to-utc
developer-guides/converting-utc-to-tai
developer-guides/checking-if-date-is-leap.rst

View file

@ -0,0 +1,14 @@
from datetime import date, timedelta
from leapseconddata import LeapSecondData
my_date = date(2015, 12, 31)
data = LeapSecondData.from_standard_source()
for leap in data.leap_seconds:
time = leap.start - timedelta(seconds=1)
if my_date.year == time.year and my_date.month == time.month and my_date.day == time.day:
print(f"{my_date} has a leap second!")
break
else:
print(f"{my_date} does not have a leap second.")

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2024 Thomas Touhey
# SPDX-License-Identifier: Unlicense
# Placed in a separate file so that it does not appear in the produced docs.

View file

@ -0,0 +1,21 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
Checking if a date has a leap second
====================================
In order to check if a date has a leap second, you must first
obtain the leap second data by using one of the methods described
in :ref:`devguide-obtaining-leaps`. Then, you can iterate over
the fetched leap seconds to check for the date.
For example, in order to check if December 31st, 2016 has a leap
second, you can use the following code:
.. literalinclude:: check-date-leap.py
The output of this program is the following:
.. code-block:: text
2016-12-31 has a leap second!

View file

@ -0,0 +1,9 @@
from datetime import datetime
from leapseconddata import LeapSecondData, tai
my_date = datetime(2024, 7, 18, 22, 0, 37, tzinfo=tai)
data = LeapSecondData.from_standard_source()
my_tai_date = data.tai_to_utc(my_date)
print(my_tai_date.isoformat())

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2024 Thomas Touhey
# SPDX-License-Identifier: Unlicense
# Placed in a separate file so that it does not appear in the produced docs.

View file

@ -0,0 +1,9 @@
from datetime import UTC, datetime
from leapseconddata import LeapSecondData
my_date = datetime(2024, 7, 18, 22, 0, 0, tzinfo=UTC)
data = LeapSecondData.from_standard_source()
my_tai_date = data.to_tai(my_date)
print(my_tai_date.isoformat())

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2024 Thomas Touhey
# SPDX-License-Identifier: Unlicense
# Placed in a separate file so that it does not appear in the produced docs.

View file

@ -0,0 +1,22 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
Converting a TAI date and time to UTC
=====================================
.. py:currentmodule:: leapseconddata
In order to convert a TAI date and time to UTC, you must first
obtain the leap second data by using one of the methods described
in :ref:`devguide-obtaining-leaps`. Then, you can use the
:py:meth:`LeapSecondData.tai_to_utc` method to convert the date and time.
For example:
.. literalinclude:: convert-tai-to-utc.py
This program will provide you with the following output:
.. code-block:: text
2024-07-18T22:00:00g+00:00

View file

@ -0,0 +1,22 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
Converting a UTC date and time to TAI
=====================================
.. py:currentmodule:: leapseconddata
In order to convert a UTC date and time to TAI, you must first
obtain the leap second data by using one of the methods described
in :ref:`devguide-obtaining-leaps`. Then, you can use the
:py:meth:`LeapSecondData.to_tai` method to convert the date and time.
For example:
.. literalinclude:: convert-utc-to-tai.py
This program will provide you with the following output:
.. code-block:: text
2024-07-18T22:00:37+00:00

View file

@ -0,0 +1,64 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
.. _devguide-obtaining-leaps:
Obtaining a list of leap seconds
================================
.. py:currentmodule:: leapseconddata
In order to obtain the current leap second list, you must use one of
:py:class:`LeapSecondData` ``from_*`` class methods.
Using the first available standard source
-----------------------------------------
If you do not have any particular restrictions on your Internet access,
you can try the "magic" method :py:meth:`LeapSecondData.from_standard_source`,
which will try known local then network sources:
.. code-block:: python
from leapseconddata import LeapSecondData
data = LeapSecondData.from_standard_source()
...
Using a custom file source
--------------------------
If you have a custom path for the ``leap-seconds.list`` the module can use,
you can use the :py:meth:`LeapSecondData.from_file` method. For example,
if your file is located at ``/etc/my-program/leap-seconds.list``:
.. code-block:: python
from leapseconddata import LeapSecondData
data = LeapSecondData.from_file("/etc/my-program/leap-seconds.list")
Using a custom URL
------------------
If you have restrictions on your Internet access and can only access the
file from a specific URL available to your machine, you can use
:py:meth:`LeapSecondData.from_url`:
.. code-block:: python
from leapseconddata import LeapSecondData
data = LeapSecondData.from_url("https://tz.example/leap-seconds.list")
You can also still try local sources before your custom URL, by using
:py:meth:`LeapSecondData.from_standard_source` with the ``custom_sources``
keyword parameter set:
.. code-block:: python
from leapseconddata import LeapSecondData
data = LeapSecondData.from_standard_source(
custom_sources=["https://tz.example/leap-seconds.list"],
)

12
docs/guides.rst Normal file
View file

@ -0,0 +1,12 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
General guides
==============
This section consists of multiple guides for solving specific problems.
.. toctree::
:maxdepth: 2
guides/install

17
docs/guides/install.rst Normal file
View file

@ -0,0 +1,17 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
.. _guide-install:
Installing leapseconddata
=========================
In order to install leapseconddata, the instructions may depend on the system
you want to install it on.
pip (generic)
-------------
leapseconddata can be installed via ``pip``::
pip install leapseconddata

View file

@ -1,9 +1,9 @@
.. SPDX-FileCopyrightText: 2021 Jeff Epler .. SPDX-FileCopyrightText: 2021 Jeff Epler
.. .. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only .. SPDX-License-Identifier: GPL-3.0-only
leapseconddata leapseconddata |version|
============== ========================
.. image:: https://github.com/jepler/leapseconddata/actions/workflows/test.yml/badge.svg .. image:: https://github.com/jepler/leapseconddata/actions/workflows/test.yml/badge.svg
:target: https://github.com/jepler/leapseconddata/actions/workflows/test.yml :target: https://github.com/jepler/leapseconddata/actions/workflows/test.yml
@ -13,10 +13,54 @@ leapseconddata
:target: https://pypi.org/project/leapseconddata :target: https://pypi.org/project/leapseconddata
:alt: PyPI :alt: PyPI
This module allows you to download and extract leap second data from
various trusted sources, both offline and online, in order to:
* Convert dates and times between TAI and UTC;
* Determine if a date has an extra second at the end in UTC.
You can also find the project in the following locations:
* `jelper/leapseconddata repository on Github
<https://github.com/jepler/leapseconddata>`_;
* `leapseconddata project on PyPI
<https://pypi.org/project/leapseconddata/>`_.
The projects code and documentation contents are licensed under GNU General
Public License version 3.
How-to guides
-------------
These sections provide guides, i.e. recipes, targeted towards various actors.
They guide you through the steps involved in addressing key problems
and use-cases.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 3
:caption: Contents:
.. automodule:: leapseconddata guides
:members: developer-guides
Discussion topics
-----------------
These sections discuss key topics and concepts at a fairly high level, and
provide useful background information and explanation.
.. toctree::
:maxdepth: 3
topics
References
----------
These sections provide technical reference for APIs and other aspects of
the project's machinery. They go into detail, and therefore, assume you
have a basic understanding of key concepts.
.. toctree::
:maxdepth: 2
code

13
docs/topics.rst Normal file
View file

@ -0,0 +1,13 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
General topics
==============
These topics explore general concepts behind the project.
.. toctree::
:maxdepth: 2
topics/leap-seconds
topics/leap-second-distribution

View file

@ -0,0 +1,50 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
How is leap second data distributed?
====================================
Leap seconds are announced **every six months** by the IERS in the
`Bulletin C publications`_:
* On beginning of January if they are introduced on June 30th (UTC);
* On beginning of July if they are introduced in December 31st (UTC).
For example:
* `Bulletin C 52`_, published on July 6th, 2016, announces a leap
second will be introduced on December 31st, 2016 (UTC).
* `Bulletin C 67`_, published on July 4th, 2024, announces **no**
leap second will be introduced on June 30th, 2024 (UTC).
The IERS also distributes a file named ``leap-seconds.list``, at the
following URL::
https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list
From here, there are multiple approaches to how systems can receive
the information, in order to display the time correctly:
* NTP supports informing clients of minutes with a leap second.
See `The NTP Timescale and Leap Seconds`_ for more information;
* Debian and derivatives distribute the file provided by the IERS, as
well as some commodities, through the tzdata_ package.
This file is available at ``/usr/share/zoneinfo/leap-seconds.list``;
* FreeBSD's ntpd has a ntpleapfetch_ command that fetches ``leap-seconds.list``
file, and stores it in ``/var/db/ntpd.leap-seconds.list``.
* Programs can fetch the file directly from network sources, if the network
is not restricted.
If using :py:meth:`LeapSecondData.from_standard_source`, ``leapseconddata``
will use local sources if available, and official network sources if
not found.
.. _Bulletin C publications:
https://datacenter.iers.org/availableVersions.php?id=16
.. _Bulletin C 52:
https://datacenter.iers.org/data/16/bulletinc-052.txt
.. _Bulletin C 67:
https://datacenter.iers.org/data/16/bulletinc-067.txt
.. _The NTP Timescale and Leap Seconds: https://www.ntp.org/reflib/leap/
.. _tzdata: https://salsa.debian.org/glibc-team/tzdata
.. _ntpleapfetch: https://docs.ntpsec.org/latest/ntpleapfetch.html

View file

@ -0,0 +1,43 @@
.. SPDX-FileCopyrightText: 2024 Thomas Touhey
.. SPDX-License-Identifier: GPL-3.0-only
What are leap seconds?
======================
`Coordinated Universal Time (UTC) <UTC_>`_ is a time standard based on two
other standards, `International Atomic Time (TAI) <TAI_>`_ and
`Universal Time (UT1) <UT1_>`_. It aims at being at a whole second offset
from TAI, while keeping UTC and UT1 within 0.9 seconds of each other.
In order to accomplish that, UTC bases itself on TAI, and gets `leap seconds`_
added to it when considered necessary by the `International Earth Rotation
Service (IERS) <IERS_>`_, in a semi-annually published bulletin called
`Bulletin C`_ which announces whether or not a leap second is inserted
in June 30th and/or December 31st, meaning the UTC clock may reach ``23:59:60``
on these dates.
.. note::
With timezones, the leap second may not be inserted at ``23:59``, but
at another time. For example:
* In France, using Central European Time (CET, UTC+01:00), the leap second
was inserted on January 1st, 2017, at ``00:59:60``.
* In Australia, using Australian Western Central Standard Time (AWCST,
UTC+08:45), the leap second was inserted on January 1st, 2017,
at ``08:44:60``.
* In the United States, using Mountain Time Zone (UTC-07:00), the leap
second was inserted on December 31st, 2016, at ``16:59:60``.
For more information, you can read `The Unix leap second mess (madore.org)
<http://www.madore.org/%7Edavid/computers/unix-leap-seconds.html>`_, as
well as the Wikipedia pages linked above.
.. _UTC: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
.. _TAI: https://en.wikipedia.org/wiki/International_Atomic_Time
.. _UT1: https://en.wikipedia.org/wiki/Universal_Time
.. _leap seconds: https://en.wikipedia.org/wiki/Leap_second
.. _IERS:
https://en.wikipedia.org/wiki/
International_Earth_Rotation_and_Reference_Systems_Service
.. _Bulletin C: https://datacenter.iers.org/productMetadata.php?id=16

View file

@ -17,7 +17,6 @@ For example, to retrieve the UTC-TAI offset on January 1, 2011:
>>> when = datetime.datetime(2011, 1, 1, tzinfo=datetime.timezone.utc) >>> when = datetime.datetime(2011, 1, 1, tzinfo=datetime.timezone.utc)
>>> ls.tai_offset(when).total_seconds() >>> ls.tai_offset(when).total_seconds()
34.0 34.0
""" """
from __future__ import annotations from __future__ import annotations

View file

@ -20,6 +20,7 @@ line-length=120
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "D", "I", "N", "UP", "YTT", "BLE", "B", "FBT", "A", "COM", "C4", "DTZ", "FA", "ISC", "ICN", "PIE", "PYI", "Q", "RET", "SIM", "TID", "TCH", "ARG", "PTH", "C", "R", "W", "FLY", "RUF", "PL"] select = ["E", "F", "D", "I", "N", "UP", "YTT", "BLE", "B", "FBT", "A", "COM", "C4", "DTZ", "FA", "ISC", "ICN", "PIE", "PYI", "Q", "RET", "SIM", "TID", "TCH", "ARG", "PTH", "C", "R", "W", "FLY", "RUF", "PL"]
ignore = ["D203", "D213", "D400", "D415", "ISC001", "COM812"] ignore = ["D203", "D213", "D400", "D415", "ISC001", "COM812"]
exclude = ["docs/**/*.py"]
[project] [project]
name = "leapseconddata" name = "leapseconddata"
authors = [{name = "Jeff Epler", email = "jepler@gmail.com"}] authors = [{name = "Jeff Epler", email = "jepler@gmail.com"}]