Skip to content

Commit

Permalink
Add documentation build (#13)
Browse files Browse the repository at this point in the history
* adopt mkdocs setup from https://github.com/secorolab/python-pkg-template
* update documentation, adopting Google-style docstrings
  • Loading branch information
minhnh authored Nov 27, 2024
1 parent 6302d3b commit 0660197
Show file tree
Hide file tree
Showing 18 changed files with 364 additions and 61 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/deploy-mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Publish docs with MkDocs via GitHub Pages
on:
push:
branches:
- main

jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[docs]"
- name: Deploy MkDocs
run: mkdocs gh-deploy --force
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# RDF Utilities

Tools for managing RDF resources and common models.
See [API documentation](reference/rdf_utils/) for more details.
6 changes: 6 additions & 0 deletions docs/styles/extra.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:root {
--md-primary-fg-color: #9d2246;
--md-primary-fg-color--light: #d50c2f;
--md-primary-fg-color--dark: #9d2246;
--md-accent-fg-color: #f39ca9;
}
76 changes: 76 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
site_name: RDF Utilities
site_description: 'A utility package for handling RDF graphs and RDF models'
site_author: 'Minh Nguyen'
repo_url: https://github.com/minhnh/rdf-utils

nav:
- Home: README.md
# defer to gen-files + literate-nav
- API reference: reference/
theme:
name: 'material'
# logo: assets/logo.png
icon:
repo: fontawesome/brands/github
palette:
# Palette toggle for automatic mode
- media: "(prefers-color-scheme)"
primary: custom
accent: custom
toggle:
icon: material/brightness-auto
name: Switch to light mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: custom
accent: custom
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: custom
accent: custom
toggle:
icon: material/brightness-4
name: Switch to system preference

plugins:
- search
- awesome-pages
- gen-files:
scripts:
- scripts/gen_api_pages.py
- literate-nav:
nav_file: SUMMARY.md
- section-index
- mkdocstrings:
default_handler: python
handlers:
python:
import:
- https://docs.python.org/3/objects.inv
- https://rdflib.readthedocs.io/en/stable/objects.inv
paths: [src]
options:
docstring_options:
ignore_init_summary: true
merge_init_into_class: true
docstring_section_style: list
separate_signature: true
heading_level: 1
summary: true

extra_css:
- styles/extra.css

extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/secorolab
- icon: material/web
link: https://www.uni-bremen.de/secoro

copyright: Copyright © 2024 SECORO Group
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ distrib = [
"numpy",
"scipy",
]
docs = [
"pathlib", # API generation script
"mkdocs-material", # material theme
"mkdocs-awesome-pages-plugin", # allow short hands for loading all markdowns
"mkdocstrings[python]", # render API pages for Python
"mkdocs-literate-nav", # summary API page
"mkdocs-gen-files", # generate API files
"mkdocs-section-index", # generate index for API
]
[project.urls]
"Homepage" = "https://github.com/minhnh/rdf-utils"

Expand Down
38 changes: 38 additions & 0 deletions scripts/gen_api_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Generate the API reference pages.
Taken from https://mkdocstrings.github.io/recipes/
"""

from pathlib import Path
import mkdocs_gen_files


root = Path(__file__).parent.parent
src = root / "src"

nav = mkdocs_gen_files.Nav()

for path in sorted(src.rglob("*.py")):
module_path = path.relative_to(src).with_suffix("")
doc_path = path.relative_to(src).with_suffix(".md")
full_doc_path = Path("reference", doc_path)

parts = tuple(module_path.parts)

if parts[-1] == "__init__":
parts = parts[:-1]
doc_path = doc_path.with_name("index.md")
full_doc_path = full_doc_path.with_name("index.md")
elif parts[-1] == "__main__":
continue

nav[parts] = doc_path.as_posix()

with mkdocs_gen_files.open(full_doc_path, "w") as fd:
ident = ".".join(parts)
fd.write(f"---\ntitle: {ident}\n---\n\n::: {ident}")

mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root))

with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
15 changes: 10 additions & 5 deletions src/rdf_utils/caching.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: MPL-2.0
"""Utilites for caching file contents"""
from socket import _GLOBAL_DEFAULT_TIMEOUT
import urllib.request

Expand All @@ -10,8 +11,9 @@
def read_file_and_cache(filepath: str) -> str:
"""Read and cache string contents of files for quick access and reducing IO operations.
May need "forgetting" mechanism if too many large files are stored. Should be fine
for loading JSON metamodels and SHACL constraints in Turtle format.
Note:
May need "forgetting" mechanism if too many large files are stored. Should be fine
for loading JSON metamodels and SHACL constraints in Turtle format.
"""
if filepath in __FILE_LOADER_CACHE:
return __FILE_LOADER_CACHE[filepath]
Expand All @@ -26,11 +28,14 @@ def read_file_and_cache(filepath: str) -> str:
return file_content


def read_url_and_cache(url: str, timeout=_GLOBAL_DEFAULT_TIMEOUT) -> str:
def read_url_and_cache(url: str, timeout: float = _GLOBAL_DEFAULT_TIMEOUT) -> str:
"""Read and cache text responses from URL
`timeout` specifies duration in seconds to wait for response. Only works for HTTP, HTTPS & FTP.
By default `socket._GLOBAL_DEFAULT_TIMEOUT` will be used, which usually means no timeout.
Parameters:
url: URL to be opened with urllib
timeout: duration in seconds to wait for response. Only works for HTTP, HTTPS & FTP.
Default: `socket._GLOBAL_DEFAULT_TIMEOUT` will be used,
which usually means no timeout.
"""
if url in __URL_CONTENT_CACHE:
return __URL_CONTENT_CACHE[url]
Expand Down
17 changes: 10 additions & 7 deletions src/rdf_utils/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,17 @@ def _load_list_re(
def load_list_re(
graph: Graph, first_node: BNode, parse_uri: bool = True, quiet: bool = True
) -> list[Any]:
"""!Recursively iterate over RDF list containers for extracting lists of lists.
"""Recursively iterate over RDF list containers for extracting lists of lists.
@param graph Graph object to extract the list(s) from
@param first_node First element in the list
@param parse_uri if True will try converting literals into URIRef
@param quiet if True will not throw exceptions other than loop detection
@exception RuntimeError Raised when a loop is detected
@exception ValueError Raised when `quiet` is `False` and short URI cannot be expanded
Parameters:
graph: Graph object to extract the list(s) from
first_node: First element in the list
parse_uri: if True will try converting literals into URIRef
quiet: if True will not throw exceptions other than loop detection
Raises:
RuntimeError: When a loop is detected
ValueError: When `quiet` is `False` and short URI cannot be expanded
"""
node_set = set()

Expand Down
24 changes: 17 additions & 7 deletions src/rdf_utils/constraints.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
# SPDX-License-Identifier: MPL-2.0
from typing import Dict
from rdflib import Dataset, Graph
import pyshacl


class ConstraintViolation(Exception):
def __init__(self, domain, message):
"""Exception for domain-specific constraint violation
Attributes:
domain: the violation's domain
"""
domain: str

def __init__(self, domain: str, message: str):
super().__init__(f"{domain} constraint violated: {message}")
self.domain = domain


class SHACLViolation(ConstraintViolation):
"""Specialized exception for SHACL violations"""
def __init__(self, violation_str: str):
super().__init__("SHACL", violation_str)


def check_shacl_constraints(graph: Graph, shacl_dict: Dict[str, str], quiet=False) -> bool:
"""
:param graph: rdflib.Graph to be checked
:param shacl_dict: mapping from SHACL path to graph format, e.g. URL -> "turtle"
:param quiet: if true will not throw an exception
def check_shacl_constraints(graph: Graph, shacl_dict: dict[str, str], quiet:bool = False) -> bool:
"""Check a graph against a collection of SHACL constraints
Parameters:
graph: rdflib.Graph to be checked
shacl_dict: mapping from SHACL path to graph format, e.g. URL -> "turtle"
quiet: if true will not throw an exception
"""
shacl_g = Dataset()
for mm_url, fmt in shacl_dict.items():
Expand Down
2 changes: 2 additions & 0 deletions src/rdf_utils/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: MPL-2.0
"""Common processing utilites for RDF graph models"""
46 changes: 37 additions & 9 deletions src/rdf_utils/models/common.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# SPDX-License-Identifier: MPL-2.0
from typing import Any, Dict, Optional, Protocol
from typing import Any, Optional, Protocol
from rdflib import URIRef, Graph, RDF


def get_node_types(graph: Graph, node_id: URIRef) -> set[URIRef]:
"""!
Get all types of a node in an RDF graph.
"""Get all types of a node in an RDF graph.
@param graph RDF graph to look up node types from
@param node_id URIRef of target node
@return set of the node's types as URIRef's
Parameters:
graph: RDF graph to look up node types from
node_id: URIRef of target node
Returns:
A set of the node's types as URIRef's
"""
types = set()
for type_id in graph.objects(subject=node_id, predicate=RDF.type):
Expand All @@ -19,11 +21,20 @@ def get_node_types(graph: Graph, node_id: URIRef) -> set[URIRef]:


class ModelBase(object):
"""All models should have an URI as ID and types"""
"""Base object for RDF graph models, enforcing all models to have an URI as ID and types.
Attributes:
id: the model's ID as an URI
types: the model's types
Parameters:
node_id: URI of the model node in the graph
graph: RDF graph for loading types if `types` is not specified
types: the model's types
"""
id: URIRef
types: set[URIRef]
_attributes: Dict[URIRef, Any]
_attributes: dict[URIRef, Any]

def __init__(
self, node_id: URIRef, graph: Optional[Graph] = None, types: Optional[set[URIRef]] = None
Expand All @@ -41,31 +52,48 @@ def __init__(
self._attributes = {}

def has_attr(self, key: URIRef) -> bool:
"""Check if the model has an attribute."""
return key in self._attributes

def set_attr(self, key: URIRef, val: Any) -> None:
"""Set an attribute value."""
self._attributes[key] = val

def get_attr(self, key: URIRef) -> Optional[Any]:
"""Get an attribute value."""
if key not in self._attributes:
return None

return self._attributes[key]


class AttrLoaderProtocol(Protocol):
"""Protocol for functions that load model attributes."""
def __call__(self, graph: Graph, model: ModelBase, **kwargs: Any) -> None: ...


class ModelLoader(object):
"""Class for dynimcally adding functions to load different model attributes."""
_loaders: list[AttrLoaderProtocol]

def __init__(self) -> None:
self._loaders = []

def register(self, loader: AttrLoaderProtocol) -> None:
"""Add a new attribute loader function.
Parameters:
loader: attribute loader function
"""
self._loaders.append(loader)

def load_attributes(self, graph: Graph, model: ModelBase, **kwargs: Any):
def load_attributes(self, graph: Graph, model: ModelBase, **kwargs: Any) -> None:
"""Load all attributes in the graph into a model with the registered loaders.
Parameters:
graph: RDF graph for loading attributes
model: Model object to load attributes into
kwargs: any keyword arguments to pass into the loader functions
"""
for loader in self._loaders:
loader(graph=graph, model=model, **kwargs)
Loading

0 comments on commit 0660197

Please sign in to comment.