From 5cd5548d46d73b5a6ee75edc86676866597ff575 Mon Sep 17 00:00:00 2001 From: Lucidiot Date: Wed, 2 Oct 2019 07:40:03 +0200 Subject: [PATCH] Add docs, closes #3 --- .gitlab-ci.yml | 23 +++- README.rst | 15 +-- docs/Makefile | 20 +++ docs/conf.py | 72 ++++++++++ docs/contributing.rst | 55 ++++++++ docs/index.rst | 211 ++++++++++++++++++++++++++++++ docs/make.bat | 35 +++++ docs/output.rst | 29 ++++ requirements-dev.txt | 2 + setup.cfg | 3 + twtxt_registry_client/__main__.py | 2 + twtxt_registry_client/output.py | 191 ++++++++++++++++++++++++++- 12 files changed, 639 insertions(+), 19 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/contributing.rst create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/output.rst diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6aab59a..2e34651 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,11 +22,16 @@ flake8: script: - flake8 +doc8: + stage: test + script: + - doc8 + deploy-pypi: stage: deploy when: manual only: - - master@Lucidiot/twtxt-registry-client + - tags@Lucidiot/twtxt-registry-client environment: name: pypi url: https://pypi.org/project/twtxt-registry-client @@ -47,7 +52,7 @@ deploy-testpypi: stage: deploy when: manual only: - - branches@Lucidiot/twtxt-registry-client + - tags@Lucidiot/twtxt-registry-client environment: name: testpypi url: https://test.pypi.org/project/twtxt-registry-client @@ -63,3 +68,17 @@ deploy-testpypi: - echo "password=$PYPI_DEPLOY_PASSWORD" >> ~/.pypirc - python setup.py sdist bdist_wheel - twine upload dist/* -r testpypi + +pages: + stage: deploy + when: manual + only: + - master@Lucidiot/twtxt-registry-client + artifacts: + paths: + - public + + script: + - cd docs + - make html + - mv _build/html ../public diff --git a/README.rst b/README.rst index 7f402df..d837455 100644 --- a/README.rst +++ b/README.rst @@ -2,19 +2,8 @@ twtxt-registry-client ===================== A simple API client for servers implementing `twtxt`_'s `registry`_ API. +`Browse documentation`_ .. _twtxt: https://github.com/buckket/twtxt .. _registry: https://twtxt.readthedocs.io/en/stable/user/registry.html - -To-do ------ - -* Error handling -* Command-line help -* Sphinx documentation - - * Doc8 linting - * GitLab Pages - -* Parsing, enhanced outputs and porcelain mode, just like with `twtxt`_ -* Verbose output and logging +.. _Browse documentation: https://lucidiot.gitlab.io/twtxt-registry-client diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..4e4cd40 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,72 @@ +from pathlib import Path +import sys + +# 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. +# +BASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, BASE_DIR / '..') + + +# -- Project information ----------------------------------------------------- + +project = 'twtxt-registry-client' +copyright = '2019, Lucidiot' +author = 'Lucidiot' + +# The full version, including alpha/beta/rc tags +with (BASE_DIR / '..' / 'VERSION').open() as f: + release = f.read().strip() + + +# -- 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', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', +] + +# 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 = 'alabaster' +html_theme_options = { + 'description': 'API client and CLI for twtxt registries', + 'fixed_sidebar': 'true', +} + +# 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'] + + +# -- Extension configuration ------------------------------------------------- + +# Concatenate the class' and __init__'s docstrings when documenting a class +autoclass_content = 'both' diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..174d4ca --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,55 @@ +Contributing +============ + +Contributions to the project are greatly appreciated. + +Bugs and suggestions +-------------------- + +You may `submit an issue`_ to GitLab to warn of any bugs, ask for new features, +or ask any questions that are not answered in this documentation. + +When reporting a bug, do not forget to put in your version of Python and your +version of *twtxt-registry-client*. This will greatly help when +troubleshooting, as most errors often come from version incompatibilities. + +Development +----------- + +Setup +^^^^^ + +You will need a virtual envionment to work properly. `virtualenvwrapper`_ is +recommended:: + + git clone https://gitlab.com/Lucidiot/twtxt-registry-client + cd twtxt-registry-client + mkvirtualenv -a . twtxt-registry-client + pip install -e .[dev] + +This will clone the repository, create a virtual environment named +``twtxt-registry-client``, then tell pip to let the package be editable +(``-e``). The ``[dev]`` suffix adds the extra requirements useful for +development. + +Linting +^^^^^^^ + +The source code follows the PEP 8 code style and performs CI checks using the +``flake8`` tool. To perform the same checks locally, run ``flake8`` on the root +directory of this repository. + +Documentation +------------- + +The documentation you are reading is generated by the `Sphinx`_ tool. +The text files that hold the documentation's contents are written in +`reStructuredText`_ and are available under the ``/docs`` folder of the +`GitLab repository`_. +They are also subject to linting using the ``doc8`` tool. + +.. _submit an issue: https://gitlab.com/Lucidiot/twtxt-registry-client/issues/new +.. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io +.. _GitLab repository: https://gitlab.com/Lucidiot/twtxt-registry-client +.. _Sphinx: http://www.sphinx-doc.org/ +.. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..fd52b4a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,211 @@ +twtxt-registry-client +===================== + +:ref:`genindex` - :ref:`modindex` - :ref:`search` + +.. image:: https://img.shields.io/pypi/v/twtxt-registry-client.svg + :target: https://pypi.org/project/twtxt-registry-client + +.. image:: https://img.shields.io/pypi/l/twtxt-registry-client.svg + :target: https://pypi.org/project/twtxt-registry-client + +.. image:: https://img.shields.io/pypi/format/twtxt-registry-client.svg + :target: https://pypi.org/project/twtxt-registry-client + +.. image:: https://img.shields.io/pypi/pyversions/twtxt-registry-client.svg + :target: https://pypi.org/project/twtxt-registry-client + +.. image:: https://img.shields.io/pypi/status/twtxt-registry-client.svg + :target: https://pypi.org/project/twtxt-registry-client + +.. image:: https://gitlab.com/Lucidiot/twtxt-registry-client/badges/master/pipeline.svg + :target: https://gitlab.com/Lucidiot/twtxt-registry-client/pipelines + +.. image:: https://requires.io/github/Lucidiot/twtxt-registry-client/requirements.svg?branch=master + :target: https://requires.io/github/Lucidiot/twtxt-registry-client/requirements/?branch=master + +.. image:: https://img.shields.io/github/last-commit/Lucidiot/twtxt-registry-client.svg + :target: https://gitlab.com/Lucidiot/twtxt-registry-client/commits + +.. image:: https://img.shields.io/badge/badge%20count-9-brightgreen.svg + :target: https://gitlab.com/Lucidiot/twtxt-registry-client + +A Python CLI for the twtxt `registry API`_. + +Installation +------------ + +This package has a very standard Python setup:: + + pip install twtxt_registry_client + +That's it, nothing more. + +Usage +----- + +Base arguments +^^^^^^^^^^^^^^ + +.. code:: + + $ twtxt-registry + [--version] + [--help] + [-k|-insecure] + [-f [raw|json|pretty]] + REGISTRY_URL + COMMAND + [subcommand args] + +``--version`` + Output the CLI's version number and exit. +``--help`` + Output the main help text and exit. +``-k`` / ``--insecure`` + Disable SSL certificate checks; first added for the `twtxt demo registry`_ + as it is appears to be unmaintained and has an expired certificate. +``-f`` / ``--format`` with one of ``raw``, ``json`` or ``pretty`` + Use a specific :class:`Formatter ` + class to output the HTTP responses. +``REGISTRY_URL`` + Base URL to a twtxt registry's API. + + .. note:: + + The recommended base path for registry APIs is at ``http://host/api``, + but the `registry API`_ specification does not enforce it. Therefore, + on most registries, you will need to append ``/api`` to the hostname. + +``COMMAND [subcommand args]`` + A client subcommand; see the subcommand-specific sections below. + +Registration +^^^^^^^^^^^^ + +.. code:: + + $ twtxt-registry + [...base arguments...] + register + [--help] + [-n|--nickname [NICK]] + [-u|--url [URL]] + +``--help`` + Output the subcommand help text and exit. +``-n [NICK]`` / ``--nickname [NICK]`` + Set a custom nickname. If omitted, the CLI will try to read it from the + ``twtxt`` client's standard configuration; this may not work with other + twtxt implementations. +``-u [URL]`` / ``--url [URL]`` + Set a custom public URL. If omitted, the CLI will try to read it from the + ``twtxt`` client's standard configuration; this may not work with other + twtxt implementations. + +This subcommand outputs the HTTP response directly, see +:meth:`Formatter.format_response() +`. + +List users +^^^^^^^^^^ + +.. code:: + + $ twtxt-registry + [...base arguments...] + users + [--help] + [-q|--query [TEXT]] + +``--help`` + Output the subcommand help text and exit. +``-q [TEXT]`` / ``--query [TEXT]`` + Optionally filter users by a query. + +This subcommand outputs the users list, see :meth:`Formatter.format_users() +`. +If the registry returns an HTTP 4xx or 5xx error code, the response is printed +directly. See :meth:`Formatter.format_response() +`. + +List tweets +^^^^^^^^^^^ + +.. code:: + + $ twtxt-registry + [...base arguments...] + tweets + [--help] + [-q|--query [TEXT]] + +``--help`` + Output the subcommand help text and exit. +``-q [TEXT]`` / ``--query [TEXT]`` + Optionally filter tweets by a query. + +This subcommand outputs the tweets list, see :meth:`Formatter.format_tweets() +`. +If the registry returns an HTTP 4xx or 5xx error code, the response is printed +directly. See :meth:`Formatter.format_response() +`. + +List tweets by mention +^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: + + $ twtxt-registry + [...base arguments...] + mentions + [--help] + [NAME_OR_URL] + +``--help`` + Output the subcommand help text and exit. +``NAME_OR_URL`` + Name or URL of a user to list mentions to. + + If a name is specified, the CLI will try to deduce its URL from the + ``twtxt`` client's configuration. If nothing is specified, the CLI will + use the configured public URL to default to the local user. + This may not work on other twtxt implementations. + +This subcommand outputs the tweets list, see :meth:`Formatter.format_tweets() +`. +If the registry returns an HTTP 4xx or 5xx error code, the response is printed +directly. See :meth:`Formatter.format_response() +`. + +List tweets by tag +^^^^^^^^^^^^^^^^^^ + +.. code:: + + $ twtxt-registry + [...base arguments...] + tag + [--help] + NAME + +``--help`` + Output the subcommand help text and exit. +``NAME`` + Tag to list tweets for. + +This subcommand outputs the tweets list, see :meth:`Formatter.format_tweets() +`. +If the registry returns an HTTP 4xx or 5xx error code, the response is printed +directly. See :meth:`Formatter.format_response() +`. + +Learn more +---------- + +.. toctree:: + output + contributing + +.. _registry API: https://twtxt.readthedocs.io/en/stable/user/registry.html +.. _twtxt demo registry: https://registry.twtxt.org diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..2119f51 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/output.rst b/docs/output.rst new file mode 100644 index 0000000..3e2dd61 --- /dev/null +++ b/docs/output.rst @@ -0,0 +1,29 @@ +Formatters +========== + +Base attributes +--------------- + +.. autoclass:: twtxt_registry_client.output.Formatter + :members: + +.. autodata:: twtxt_registry_client.output.registry + :annotation: + +.. autoclass:: twtxt_registry_client.output.FormatterMetaclass + :members: + +.. autoclass:: twtxt_registry_client.output.FormatterRegistry + :members: + +Standard formatters +------------------- + +.. autoclass:: twtxt_registry_client.output.RawFormatter + :members: + +.. autoclass:: twtxt_registry_client.output.JSONFormatter + :members: + +.. autoclass:: twtxt_registry_client.output.PrettyFormatter + :members: diff --git a/requirements-dev.txt b/requirements-dev.txt index 8d7f646..1d3dfb8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,3 @@ flake8>=3.7 +Sphinx>=2.2 +doc8>=0.8 diff --git a/setup.cfg b/setup.cfg index df20b75..b963157 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [flake8] exclude = .git,__pycache__,docs,*.pyc,venv + +[doc8] +ignore-path=**/*.txt,*.txt,*.egg-info,docs/_build,venv,.git diff --git a/twtxt_registry_client/__main__.py b/twtxt_registry_client/__main__.py index f917081..046c0dd 100755 --- a/twtxt_registry_client/__main__.py +++ b/twtxt_registry_client/__main__.py @@ -52,11 +52,13 @@ def cli(ctx, registry_url, insecure, format): '-n', '--nickname', help='Nickname to register with. ' 'Defaults to the configured twtxt nickname, if available.', + metavar='[NICK]', ) @click.option( '-u', '--url', help='URL to the twtxt file to register with. ' 'Defaults to the configured twtxt URL, if available.', + metavar='[URL]', ) @click.pass_context def register(ctx, nickname, url): diff --git a/twtxt_registry_client/output.py b/twtxt_registry_client/output.py index e7fbf43..a23d2a8 100644 --- a/twtxt_registry_client/output.py +++ b/twtxt_registry_client/output.py @@ -10,34 +10,113 @@ import textwrap class FormatterRegistry(ClassRegistry): + """ + The class that holds registered formatters; allows registering a formatter + automatically by merely importing it. + + You should not have to use this class directly; use the already + instanciated :data:`registry` in this module instead. + """ def check_value(self, value): + """ + Ensure that a new formatter class subclasses :class:`Formatter`. + + :param Callable value: A formatter subclass. + :raises AssertionError: + When the formatter subclass does not subclass :class:`Formatter`. + """ assert issubclass(value, Formatter), 'Can only register formatters' registry = FormatterRegistry() +""" +The formatter registry: an enhanced ``dict`` which holds links between +formatter names (used in the ``--format`` command-line argument) and formatter +classes. +""" class FormatterMetaclass(registry.metaclass, ABCMeta): - pass + """ + The metaclass which allows auto-registration of each formatter. + In most cases, you should not have to use this class directly; + use the :class:`Formatter` abstract class instead. + Registration of classes that do not subclass :class:`Formatter` will fail. + """ class Formatter(metaclass=FormatterMetaclass, register=False): + """ + Abstract base class for output formatters. + + When creating a new subclass, you may specify some parameters to pass to + the auto-registration system:: + + class MyFormatter(key='something'): + pass + + class MyInvisibleFormatter(register=False): + pass + + In the above example, ``MyFormatter`` can be used in the command line + client using ``twtxt-registry -f something``, and MyInvisibleFormatter + will not be visible directly (which is useful for abstract classes). + + ``register`` defaults to ``True``, and ``key`` defaults to the class name. + """ + # TODO: Add link to objtools docs here once they are published @abstractmethod def format_response(self, resp): - pass + """ + Generic output for an HTTP response: generally, this would include + the HTTP status code and the response body. This is used to output + HTTP errors or basic requests which do not have a very meaningful + response body, like the registration API. + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the formatter's output. + :rtype: str + """ @abstractmethod def format_tweets(self, resp): - pass + """ + Output tweets from a successful HTTP response. The tweets can be + obtained from ``resp.text`` and parsing of the response text is left + to the formatter. + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the formatter's output. + :rtype: str + """ @abstractmethod def format_users(self, resp): - pass + """ + Output users from a successful HTTP response. The users can be obtained + from ``resp.text`` and parsing of the response text is left to the + formatter. + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the formatter's output. + :rtype: str + """ class RawFormatter(Formatter, key='raw'): + """ + A very basic formatter which always outputs the response's body directly. + + Use ``-f raw`` or ``--format raw`` in the CLI to select it. + """ def format_response(self, resp): return resp.text @@ -50,8 +129,30 @@ class RawFormatter(Formatter, key='raw'): class JSONFormatter(Formatter, key='json'): + """ + A formatter which always returns valid JSON documents. + + Use ``-f json`` or ``--format json`` in the CLI to select it. + """ def format_response(self, resp): + """ + Outputs a simple JSON payload for any HTTP response, including its + HTTP status code, its URL and its body. + Sample output with a 404 error:: + + { + "status_code": 404, + "url": "http://somewhere/api/not/found", + "body": "Page Not Found!" + } + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the JSON output. + :rtype: str + """ return json.dumps({ 'status_code': resp.status_code, 'url': resp.url, @@ -59,6 +160,26 @@ class JSONFormatter(Formatter, key='json'): }) def format_tweets(self, resp): + """ + Outputs a list of JSON objects for an HTTP response holding tweets, + with the users' nickname and URL, the tweet's timestamp, and its + content. Sample output:: + + [ + { + "nick": "lucidiot", + "url": "https://tilde.town/~lucidiot/twtxt.txt", + "timestamp": "2019-02-31T13:37:42.123456Z", + "message": "Hello, world!" + } + ] + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the JSON output. + :rtype: str + """ if not resp.ok: return self.format_response(resp) output = [] @@ -73,6 +194,24 @@ class JSONFormatter(Formatter, key='json'): return json.dumps(output) def format_users(self, resp): + """ + Outputs a list of JSON objects for an HTTP response holding users, + with their nickname, URL, and last update timestamp. Sample output:: + + [ + { + "nick": "lucidiot", + "url": "https://tilde.town/~lucidiot/twtxt.txt", + "timestamp": "2019-02-31T13:37:42.123456Z" + } + ] + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the JSON output. + :rtype: str + """ if not resp.ok: return self.format_response(resp) output = [] @@ -87,6 +226,12 @@ class JSONFormatter(Formatter, key='json'): class PrettyFormatter(Formatter, key='pretty'): + """ + A formatter with pretty-printing for beautiful command line output. + + This is the default formatter; Use ``-f pretty`` or ``--format pretty`` + in the CLI to explicitly select it. + """ status_colors = { 1: 'white', @@ -97,6 +242,24 @@ class PrettyFormatter(Formatter, key='pretty'): } def format_response(self, resp): + """ + Outputs an HTTP response in a syntax similar to a true HTTP response, + with its status code, reason and body: + + HTTP **404 Not Found** + + Page Not Found! + + The HTTP status code may be coloured if the terminal supports it: + white for 1xx, green for 2xx, cyan for 3xx, red for 4xx and magenta + for 5xx. + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the human-readable output. + :rtype: str + """ return 'HTTP {code} {name}\n\n{body}'.format( code=click.style( str(resp.status_code), @@ -108,6 +271,16 @@ class PrettyFormatter(Formatter, key='pretty'): ) def format_tweets(self, resp): + """ + Outputs an HTTP response as a list of tweets, in a format similar to + the output of the original ``twtxt`` CLI. + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the human-readable output. + :rtype: str + """ if not resp.ok: return self.format_response(resp) @@ -152,6 +325,16 @@ class PrettyFormatter(Formatter, key='pretty'): return '\n\n'.join(output) def format_users(self, resp): + """ + Outputs an HTTP response as a list of users, in a format similar to + the output of the original ``twtxt`` CLI. + + :param resp: + A requests ``Response`` instance from an API request to a registry. + :type resp: requests.Response + :returns: A string holding the human-readable output. + :rtype: str + """ if not resp.ok: return self.format_response(resp) output = []