Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
097afc8
add .dir-locals.el to gitignore
mbruns91 Feb 19, 2026
354cdac
set up new project structure
mbruns91 Feb 20, 2026
5517731
fill exceptions.py with fundamentals
mbruns91 Feb 20, 2026
4b76331
fill url.py with url-construction tools
mbruns91 Feb 20, 2026
87d4f00
add _version.py to .gitignore
mbruns91 Feb 20, 2026
a6c22f1
write purpose of courier/courier/http/url.py
mbruns91 Feb 20, 2026
29dcc59
write purpose of courier/exceptions.py
mbruns91 Feb 20, 2026
0241f36
fill http/auth.py with authentication mechanism (for now only bearer …
mbruns91 Feb 20, 2026
f143319
write http/session.py to provide tool for creating a requests.Session
mbruns91 Feb 20, 2026
c335204
fill http/request.py with structures to perform requests
mbruns91 Feb 20, 2026
1f1890d
create base class for clients in base_client.py
mbruns91 Feb 20, 2026
f8ea107
adjust __init__.py
mbruns91 Feb 20, 2026
3e11eac
minor chnages to exceptions.py
mbruns91 Feb 20, 2026
75a42e2
minor changes to base_client.py
mbruns91 Feb 20, 2026
6fab526
write http/__init__.py
mbruns91 Feb 20, 2026
7a62123
write services/__init__.py
mbruns91 Feb 20, 2026
51bfd1e
write services/ontodocker/__init__.py
mbruns91 Feb 20, 2026
9dac8ed
correct services/ontodocker/__init__.py
mbruns91 Feb 20, 2026
f72cc1a
write _compat.py
mbruns91 Feb 20, 2026
23b476c
write models.py
mbruns91 Feb 20, 2026
e5c3936
write endpoints.py
mbruns91 Feb 20, 2026
ccc0d7d
write datasets.py
mbruns91 Feb 20, 2026
2323f8c
write sparql.py
mbruns91 Feb 20, 2026
e8907cf
write client.py
mbruns91 Feb 20, 2026
b143390
remove ontodocker.py
mbruns91 Feb 20, 2026
01b9611
update courier/__ini__.py
mbruns91 Feb 23, 2026
4b45601
update docstring in courier/http/__init__.py
mbruns91 Feb 23, 2026
7577484
courier/services/ontodocker/datasets.py: reformatting; extend 'downlo…
mbruns91 Feb 23, 2026
3fdabcd
fix download_tutlefile()
mbruns91 Feb 23, 2026
fc077a7
update courier/services/ontodocker/datasets.py: more concise error me…
mbruns91 Feb 24, 2026
f7ef664
add jupyterhub.log to .gitignore
mbruns91 Feb 24, 2026
5e49705
extend BASECliend to accept http headers for all requests
mbruns91 Feb 24, 2026
2d99e62
implement a 'query_raw()' method for OntodockerClient.sparql which al…
mbruns91 Feb 24, 2026
7550cb8
remove __version__ attribute
mbruns91 Feb 25, 2026
5360df2
add upload_graph() to services/ontodocker/datasets.py
mbruns91 Feb 25, 2026
10b9aff
Resolve conflict
samwaseda Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ test_times.dat
core.*
*.~undo-tree~
.#*
\#*#
\#*#
.dir-locals.el
_version.py
jupterhub.log
26 changes: 8 additions & 18 deletions courier/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""
courier: Interfaces for publishing workflow recipes, instances and related assets.

Public API is intentionally small; import service clients from `courier.services`.
"""

import importlib.metadata

try:
Expand All @@ -7,22 +13,6 @@
# Repository clones will register an unknown version
__version__ = "0.0.0+unknown"

from courier.ontodocker import (
create_empty_dataset,
delete_dataset,
download_dataset_as_turtle_file,
extract_dataset_names,
get_all_dataset_sparql_endpoints,
rectify_endpoints,
upload_turtlefile,
)
from courier.services.ontodocker import OntodockerClient

__all__ = [
"rectify_endpoints",
"get_all_dataset_sparql_endpoints",
"extract_dataset_names",
"download_dataset_as_turtle_file",
"create_empty_dataset",
"upload_turtlefile",
"delete_dataset",
]
__all__ = ["OntodockerClient"]
127 changes: 127 additions & 0 deletions courier/base_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# base class for clients supported by courier

from typing import Any

import requests

from courier.http.auth import bearer_headers
from courier.http.request import read_json, read_text
from courier.http.session import create_session
from courier.http.url import normalize_base_url


class BaseClient:
"""
Base client providing shared HTTP behavior and address normalization.

Parameters
----------
address
Server address as host[:port] or URL including scheme.
token
Optional bearer token.
default_scheme
Scheme used if `address` does not include one.
verify
TLS verification passed to `requests` (True/False or path to CA bundle).
timeout
Request timeout in seconds (float) or (connect, read) tuple.
session
Optional externally managed requests session.
"""

def __init__(
self,
address: str,
*,
token: str | None = None,
default_scheme: str = "https",
verify: bool | str = True,
timeout: float | tuple[float, float] = 30.0,
session: requests.Session | None = None,
) -> None:
self.address = address
self.token = token
self.default_scheme = default_scheme
self.verify = verify
self.timeout = timeout

self.base_url = normalize_base_url(
self.address, default_scheme=self.default_scheme
)

self.session = session if session is not None else create_session()
self.session.headers.update(bearer_headers(self.token))

def _request(
self,
method: str,
url: str,
*,
params: dict[str, Any] | None = None,
json: Any | None = None,
data: Any | None = None,
files: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
stream: bool = False,
) -> requests.Response:
return self.session.request(
method=method,
url=url,
params=params,
json=json,
data=data,
files=files,
headers=headers,
timeout=self.timeout,
verify=self.verify,
stream=stream,
)

def _get_text(
self,
url: str,
*,
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> str:
return read_text(self._request("GET", url, params=params, headers=headers))

def _get_json(
self,
url: str,
*,
params: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> Any:
return read_json(self._request("GET", url, params=params, headers=headers))

def _put_text(
self,
url: str,
*,
data: Any | None = None,
json: Any | None = None,
headers: dict[str, str] | None = None,
) -> str:
return read_text(
self._request("PUT", url, data=data, json=json, headers=headers)
)

def _post_text(
self,
url: str,
*,
data: Any | None = None,
json: Any | None = None,
files: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> str:
return read_text(
self._request(
"POST", url, data=data, json=json, files=files, headers=headers
)
)

def _delete_text(self, url: str, *, headers: dict[str, str] | None = None) -> str:
return read_text(self._request("DELETE", url, headers=headers))
36 changes: 36 additions & 0 deletions courier/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# central source for exceptions thrown by courier

from dataclasses import dataclass
from typing import Any


class CourierError(Exception):
"""Base exception for courier."""


class InvalidAddressError(CourierError, ValueError):
"""Raised when a provided server address cannot be normalized."""


class ValidationError(CourierError, ValueError):
"""Raised when user input is invalid (e.g. empty dataset name)."""


@dataclass
class HttpError(CourierError):
"""Raised when an HTTP request fails."""

method: str
url: str
status_code: int | None = None
message: str | None = None
response_text: str | None = None
payload: Any | None = None

def __str__(self) -> str:
parts = [f"{self.method} {self.url}"]
if self.status_code is not None:
parts.append(f"status={self.status_code}")
if self.message:
parts.append(self.message)
return " | ".join(parts)
9 changes: 9 additions & 0 deletions courier/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Shared HTTP utilities for courier.
"""

from courier.http.auth import bearer_headers
from courier.http.session import create_session
from courier.http.url import join_url, normalize_base_url

__all__ = ["bearer_headers", "create_session", "join_url", "normalize_base_url"]
22 changes: 22 additions & 0 deletions courier/http/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# authentication mechanisms used by courier

from collections.abc import Mapping

Check failure on line 3 in courier/http/auth.py

View workflow job for this annotation

GitHub Actions / pyiron / ruff-check

ruff (F401)

courier/http/auth.py:3:29: F401 `collections.abc.Mapping` imported but unused help: Remove unused import: `collections.abc.Mapping`


def bearer_headers(token: str | None) -> dict[str, str]:
"""
Construct Authorization headers for a bearer token.

Parameters
----------
token
Bearer token or None.

Returns
-------
headers
Header dict. Empty if `token` is None/blank.
"""
if token and token.strip():
return {"Authorization": f"Bearer {token.strip()}"}
return {}
93 changes: 93 additions & 0 deletions courier/http/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# request/response handling

from typing import Any

import requests

from courier.exceptions import HttpError


def raise_for_status_with_body(resp: requests.Response) -> None:
"""
Raise `HttpError` if the response indicates an HTTP error.

Parameters
----------
resp
Response object.

Raises
------
HttpError
If status code is 4xx/5xx. Includes response text where available.
"""
try:
resp.raise_for_status()
except requests.HTTPError as e:
text = None
try:
text = resp.text
except Exception:
text = None
raise HttpError(
method=resp.request.method if resp.request else "HTTP",
url=resp.url,
status_code=resp.status_code,
message=str(e),
response_text=text,
) from e


def read_json(resp: requests.Response) -> Any:
"""
Decode JSON response after checking status.

Parameters
----------
resp
Response object.

Returns
-------
payload
Parsed JSON payload.

Raises
------
HttpError
If status indicates error or JSON decoding fails.
"""
raise_for_status_with_body(resp)
try:
return resp.json()
except Exception as e:
raise HttpError(
method=resp.request.method if resp.request else "HTTP",
url=resp.url,
status_code=resp.status_code,
message="Failed to decode JSON response.",
response_text=getattr(resp, "text", None),
) from e


def read_text(resp: requests.Response) -> str:
"""
Decode text response after checking status.

Parameters
----------
resp
Response object.

Returns
-------
text
Response body as text.

Raises
------
HttpError
If status indicates error.
"""
raise_for_status_with_body(resp)
return resp.text
23 changes: 23 additions & 0 deletions courier/http/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# create requests.Session used for http request done by courier

import requests


def create_session(*, headers: dict[str, str] | None = None) -> requests.Session:
"""
Create a configured `requests.Session`.

Parameters
----------
headers
Default headers to apply to the session.

Returns
-------
session
A `requests.Session` instance.
"""
s = requests.Session()
if headers:
s.headers.update(headers)
return s
Loading
Loading