Skip to content

Commit

Permalink
Document Python API (resolve #791)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryneeverett committed Nov 2, 2024
1 parent 8a09eb7 commit 548a4b7
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 160 deletions.
37 changes: 18 additions & 19 deletions bugwarrior/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
from .load import BUGWARRIORRC, get_config_path, load_config
from .schema import (ConfigList,
ExpandedPath,
NoSchemeUrl,
"""
Config API
----------
"""
from .data import BugwarriorData
from .load import BUGWARRIORRC, get_config_path, load_config # noqa: F401
from .schema import (ConfigList, # noqa: F401
ExpandedPath, # noqa: F401
LoggingPath, # noqa: F401
MainSectionConfig,
NoSchemeUrl, # noqa: F401
ServiceConfig,
StrippedTrailingSlashUrl)
from .secrets import get_keyring, get_service_password
StrippedTrailingSlashUrl, # noqa: F401
TaskrcPath) # noqa: F401
from .secrets import get_keyring # noqa: F401

# NOTE: __all__ determines the stable, public API.
__all__ = [
# load
'BUGWARRIORRC',
'get_config_path',
'load_config',
# schema
'ConfigList',
'ExpandedPath',
'NoSchemeUrl',
'ServiceConfig',
'StrippedTrailingSlashUrl',
# secrets
'get_keyring',
'get_service_password',
BugwarriorData.__name__,
MainSectionConfig.__name__,
ServiceConfig.__name__,
]
30 changes: 21 additions & 9 deletions bugwarrior/config/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import subprocess
import typing

from lockfile.pidlockfile import PIDLockFile

Expand Down Expand Up @@ -28,31 +29,42 @@ def get_data_path(taskrc):


class BugwarriorData:
""" Local data storage.
This exposes taskwarrior's `data.location` configuration value, as well as
an interface to the ``bugwarrior.data`` file which serves as an arbitrary
key-value store.
"""
def __init__(self, data_path):
self.datafile = os.path.join(data_path, 'bugwarrior.data')
self.lockfile = os.path.join(data_path, 'bugwarrior-data.lockfile')
self._datafile = os.path.join(data_path, 'bugwarrior.data')
self._lockfile = os.path.join(data_path, 'bugwarrior-data.lockfile')
#: Taskwarrior's ``data.location`` configuration value. If necessary,
#: services can manage their own files here.
self.path = data_path

def get_data(self):
with open(self.datafile) as jsondata:
def get_data(self) -> dict:
""" Return all data from the ``bugwarrior.data`` file. """
with open(self._datafile) as jsondata:
return json.load(jsondata)

def get(self, key):
def get(self, key) -> typing.Any:
""" Return a value stored in the ``bugwarrior.data`` file. """
try:
return self.get_data()[key]
except OSError: # File does not exist.
return None

def set(self, key, value):
with PIDLockFile(self.lockfile):
""" Set a value in the ``bugwarrior.data`` file. """
with PIDLockFile(self._lockfile):
try:
data = self.get_data()
except OSError: # File does not exist.
with open(self.datafile, 'w') as jsondata:
with open(self._datafile, 'w') as jsondata:
json.dump({key: value}, jsondata)
else:
with open(self.datafile, 'w') as jsondata:
with open(self._datafile, 'w') as jsondata:
data[key] = value
json.dump(data, jsondata)

os.chmod(self.datafile, 0o600)
os.chmod(self._datafile, 0o600)
10 changes: 8 additions & 2 deletions bugwarrior/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class PydanticConfig(pydantic.v1.BaseConfig):


class MainSectionConfig(pydantic.v1.BaseModel):
""" The :ref:`common_configuration:Main Section` configuration, plus computed attributes: """

class Config(PydanticConfig):
arbitrary_types_allowed = True
Expand All @@ -110,9 +111,11 @@ class Config(PydanticConfig):
targets: ConfigList

# added during configuration loading
#: Interactive status.
interactive: bool

# added during validation (computed field support will land in pydantic-2)
#: Local data storage.
data: typing.Optional[BugwarriorData] = None

@pydantic.v1.root_validator
Expand Down Expand Up @@ -290,7 +293,10 @@ def validate_config(config: dict, main_section: str, config_path: str) -> dict:


class ServiceConfig(_ServiceConfig): # type: ignore # (dynamic base class)
""" Base class for service configurations. """
""" Pydantic_ base class for service configurations.
.. _Pydantic: https://docs.pydantic.dev/latest/
"""
Config = PydanticConfig

# Added during validation (computed field support will land in pydantic-2)
Expand Down Expand Up @@ -327,7 +333,7 @@ def compute_templates(cls, values):
project_template = myprojectname
The above would cause all issues to recieve a project name
The above would cause all issues to receive a project name
of 'myprojectname', regardless of what the project name of the
generated issue was.
Expand Down
17 changes: 16 additions & 1 deletion bugwarrior/docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosectionlabel',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
Expand Down Expand Up @@ -133,6 +134,17 @@
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False

# Show the class constructor arguments under __init__ rather than in the
# header. This way we can omit __init__ when we don't want to document it.
autodoc_class_signature = "separated"

# Allow duplicate section names across the document collection (e.g., for each
# service).
autosectionlabel_prefix_document = True

# For autodoc, split parameters onto separate lines when they exceed this
# length.
maximum_signature_line_length = 79

# -- Options for HTML output ----------------------------------------------

Expand Down Expand Up @@ -302,4 +314,7 @@


# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'requests': ('https://requests.readthedocs.io/en/latest/', None),
}
2 changes: 0 additions & 2 deletions bugwarrior/docs/getting.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
Getting bugwarrior
==================

.. _requirements:

Requirements
------------

Expand Down
17 changes: 17 additions & 0 deletions bugwarrior/docs/other-services/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Python API
==========

The interfaces documented here are considered stable. All other interfaces
should be considered private to bugwarrior and are subject to change without
warning, release notes, or semantic version bumping.

.. automodule:: bugwarrior.services
:members:
:exclude-members: __init__
:member-order: bysource

.. automodule:: bugwarrior.config
:members:
:exclude-members: __init__,compute_templates
:imported-members:
:member-order: bysource
35 changes: 30 additions & 5 deletions bugwarrior/docs/other-services/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,25 @@ More likely you'll be writing your own client using an http API, so start off by
This example of accessing a local service is quite simple, but you'll likely need to pass additional arguments and perhaps go through a handshake process to authenticate to a remote server.

2. Service File
---------------
2. Initialize Service
---------------------

Add a python file with the name of your service in ``bugwarrior/services``.
There are two approaches here, depending on whether your service will be maintained in bugwarrior or will be maintained separately as a :doc:`third party service <third_party>`.

If you're sure you're going to be upstreaming your service, clone the bugwarrior repo and create a python file with the name of your service in ``bugwarrior/services``.

.. code:: bash
touch bugwarrior/services/gitbug.py
If you're going to maintain your service in it's own repository or if you're uncertain if it will be accepted upstream, create a new package for it.

.. code:: bash
cd $MY_PROJECTS
mkdir bugwarrior-gitbug
cd bugwarrior-gitbug
touch bugwarrior_gitbug.py
3. Imports
----------
Expand All @@ -48,6 +58,8 @@ Fire up your favorite editor and import the base classes and whatever library yo
log = logging.getLogger(__name__)
We're going to step through the use of these bugwarrior classes in subsequent sections, but for reference you may find the :doc:`API docs <api>` helpful.


4. Configuration Schema
-----------------------
Expand Down Expand Up @@ -208,15 +220,28 @@ The ``issues`` method is a generator which yields individual issue dictionaries.
7. Service Registration
-----------------------

Add your service class as an ``entry_point`` under the ``[bugwarrior.service]`` section in ``setup.py``.
If you're developing your service in a separate package, it's time to create a ``setup.py`` if you have not done so already, and register the name of your service with the path to your ``Service`` class.

.. code:: python
gitbug=bugwarrior.services.gitbug:GitBugService
setup(...
entry_points="""
[bugwarrior.service]
gitbug=bugwarrior_gitbug:GitBugService
"""
)
If you're developing in the bugwarrior repo, you can simply add your entry to the existing ``[bugwarrior.service]`` group.

8. Tests
----------

.. note::

The remainder of this tutorial is not geared towards third-party services. While you are free to use bugwarrior's testing infrastructure, no attempt is being made to maintain the stability of these interfaces at this time.



Create a test file and implement at least the minimal service tests by inheriting from ``AbstractServiceTest``.

.. code:: bash
Expand Down
3 changes: 2 additions & 1 deletion bugwarrior/docs/other_services.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ Other Services

.. toctree::

other-services/third-party.rst
other-services/third_party.rst
other-services/tutorial.rst
other-services/api.rst
Loading

0 comments on commit 548a4b7

Please sign in to comment.