"""
Support for accessing data in a repository with some "known locations" via an `API` object.
"""
import re
import pathlib
import functools
import webbrowser
from clldutils.path import git_describe
from clldutils.metadata import Metadata
from clldutils.jsonlib import load
VERSION_NUMBER_PATTERN = re.compile(
r'v(?P<number>(?P<major>[0-9]+)\.(?P<minor>[0-9]+)(\.(?P<patch>[0-9]+))?)$')
[docs]def assert_release(repos) -> str:
"""Make sure a git repository is checked out to a release tag."""
match = VERSION_NUMBER_PATTERN.match(git_describe(repos))
assert match, 'Repository is not checked out to a valid release tag'
return match.group('number') # pragma: no cover
[docs]class API:
"""
An API base class to provide programmatic access to data in a git repository.
:ivar pathlib.Path repos: Path of the (data) repository.
Some repositories provide functionality to explore their data, implemented as Javascript
app that can be browsed locally, from the filesystem. This scenario is supported as follows:
- the app is assumed to "live" in the :meth:`API.appdir` sub-directory,
- browsable at `<API.appdir>/index.html`,
- with app data at :meth:`API.appdatadir`.
Then, a function which recreates the app data can be decorated with :meth:`API.app_wrapper`
to initialise the app data directory and open the app in a browser after the method has
finished.
.. code-block:: python
@API.app_wrapper
def f(args):
import shutil
shutil.copy(args.api.path('stuff'), args.api.appdatadir)
"""
#: A light-weight way to specifiy a default repository location (without having to
#: overwrite __init__)
__repos_path__ = None
__default_metadata__ = None
def __init__(self, repos=None):
self.repos = pathlib.Path(repos or self.__repos_path__)
def __str__(self):
name = self.repos.resolve().name if self.repos.exists() else self.repos.name
return f'<{name} repository {git_describe(self.repos)} at {self.repos}>'
[docs] def path(self, *comps: str) -> pathlib.Path:
"""
A path within the repository.
:param comps: path components relative to `self.repos`.
"""
return self.repos.joinpath(*comps)
@functools.cached_property
def dataset_metadata(self) -> Metadata:
"""
If a repository provides metadata about the dataset curated there as JSON-LD file called
`metadata.json`, this property returns a :class:`clldutils.metadata.Metadata` object,
loaded from this metadata.
"""
mdp = self.repos / 'metadata.json'
return Metadata.from_jsonld(
load(mdp) if mdp.exists() else {}, defaults=self.__default_metadata__)
def assert_release(self): # pylint: disable=C0116
return assert_release(self.repos)
@property
def appdir(self) -> pathlib.Path: # pylint: disable=C0116
return self.path('app')
@property
def appdatadir(self) -> pathlib.Path: # pylint: disable=C0116
return self.appdir.joinpath('data')
[docs] @classmethod
def app_wrapper(cls, func):
"""Recreate appdata if requested, open app index.html in browser."""
@functools.wraps(func)
def wrapper(args):
if isinstance(args.repos, cls):
api = args.repos
else:
api = cls(args.repos)
if (not api.appdatadir.exists()) \
or getattr(args, 'recreate', False) \
or getattr(args, 'force', False) \
or (hasattr(args, 'args') and '--recreate' in args.args):
api.appdatadir.mkdir(exist_ok=True)
args.api = api
func(args)
index = api.appdir / 'index.html'
if index.exists():
webbrowser.open(index.resolve().as_uri())
return wrapper