---
src/sourcehut/client.py | 64 ++++++++++++++++++++++++++++++--
src/sourcehut/services/_base.py | 19 ++++++++++
src/sourcehut/services/builds.py | 18 ++++++++-
src/sourcehut/services/git.py | 22 +++++++++++
src/sourcehut/services/lists.py | 13 +++++++
src/sourcehut/services/meta.py | 38 +++++++++++++++++++
src/sourcehut/services/pages.py | 46 +++++++++++++++++++++--
src/sourcehut/services/paste.py | 27 ++++++++++++++
src/sourcehut/services/todo.py | 34 +++++++++++++++++
9 files changed, 273 insertions(+), 8 deletions(-)
diff --git a/src/sourcehut/client.py b/src/sourcehut/client.py
index d0c3bc5..ea83720 100644
--- a/src/sourcehut/client.py
+++ b/src/sourcehut/client.py
@@ -27,6 +27,10 @@ _NOT_FOUND = "the requested element is null which the schema does not allow"
class SRHT_SERVICE(str, Enum):
+ """
+ Sourcehut services
+ """
+
GIT = "git"
HG = "hg"
BUILDS = "builds"
@@ -44,6 +48,10 @@ class SRHT_SERVICE(str, Enum):
class VISIBILITY(str, Enum):
+ """
+ Visibility options shared across service APIs
+ """
+
PUBLIC = "PUBLIC"
UNLISTED = "UNLISTED"
PRIVATE = "PRIVATE"
@@ -53,6 +61,10 @@ class VISIBILITY(str, Enum):
class APIVersion(NamedTuple):
+ """
+ Sourcehut API version shared across service APIs
+ """
+
major: int
minor: int
patch: int
@@ -62,7 +74,15 @@ class APIVersion(NamedTuple):
@dataclasses.dataclass(frozen=True)
-class _FileUpload:
+class FileUpload:
+ """
+ Represents a GraphQL file upload
+
+ Args:
+ content:
+ Content to upload as `str`, `bytes`, or a `bytes` stream
+ """
+
content: _FileContentType
filename: str | os.PathLike[str] | None
@@ -70,10 +90,13 @@ class _FileUpload:
return (str(self.filename), self.content) if self.filename else self.content
+_FileUpload = FileUpload
+
+
def _get_upload_data(
query: str,
variables: dict[str, Any],
- uploads: dict[str, _FileUpload | Sequence[_FileUpload]],
+ uploads: dict[str, FileUpload | Sequence[FileUpload]],
) -> tuple[
# data
dict[str, dict[str, Any]],
@@ -86,7 +109,7 @@ def _get_upload_data(
data_map: dict[str, list[str]] = {}
for variable, file in uploads.items():
- if isinstance(file, _FileUpload):
+ if isinstance(file, FileUpload):
files[str(file_index)] = file._get_tuple()
data_map[str(file_index)] = [f"variables.{variable}"]
variables[variable] = variables.get(variable, None)
@@ -138,11 +161,18 @@ class SrhtClient:
def from_config(
cls: type[_ClientT], config: SrhtConfig, client: httpx.AsyncClient | None = None
) -> _ClientT:
+ """
+ Create a Sourcehut client from a
+ [`SrhtConfig`][sourcehut.config.SrhtConfig] object
+ """
if not config.api_token:
raise ValueError("api_token is not provided in the config")
return cls(config.baseurl, config.api_token, client, protocol=config.protocol)
def get_endpoint(self, service: SRHT_SERVICE) -> str:
+ """
+ Get a endpoint for a particular [`SRHT_SERVICE`][sourcehut.client.SRHT_SERVICE]
+ """
return f"{self.protocol}{service}.{self.baseurl}/query"
@property
@@ -157,6 +187,27 @@ class SrhtClient:
*,
extra_params: dict[str, Any] | None = None,
) -> dict[str, Any]:
+ """
+ Perform a GraphQL query and return the data payload.
+
+ Args:
+ service:
+ The [`SRHT_SERVICE`][sourcehut.client.SRHT_SERVICE] to query
+ variables:
+ GraphQL query variables
+ extra_params:
+ Extra parameters to pass to `httpx.AsyncClient.post()`
+
+ Returns:
+ A dictionary of the query's `data` response payload
+
+ Raises:
+ SrhtClientError:
+ The query returned a non-2XX return code and/or contained an
+ `error` responde payload
+ ResourceNotFoundError:
+ A resource was not found
+ """
payload = {"query": query, "variables": variables or {}}
# data = {"operations": json_dumps(payload)}
extra_params = extra_params or {}
@@ -178,7 +229,7 @@ class SrhtClient:
service: SRHT_SERVICE,
query: str,
variables: dict[str, Any],
- uploads: dict[str, _FileUpload | Sequence[_FileUpload]],
+ uploads: dict[str, FileUpload | Sequence[FileUpload]],
*,
extra_params: dict[str, Any] | None = None,
) -> dict[str, Any]:
@@ -215,6 +266,10 @@ class SrhtClient:
return json["data"]
async def whoami(self, service: SRHT_SERVICE) -> str:
+ """
+ Returns:
+ The authenticated username, minus the `~`
+ """
query = """
query {
me {
@@ -260,4 +315,5 @@ __all__ = (
"APIVersion",
"SrhtClient",
"DEFAULT_BASEURL",
+ "FileUpload",
)
diff --git a/src/sourcehut/services/_base.py b/src/sourcehut/services/_base.py
index 93cf5d5..cdc8e66 100644
--- a/src/sourcehut/services/_base.py
+++ b/src/sourcehut/services/_base.py
@@ -58,6 +58,25 @@ class _ServiceClient(metaclass=ABCMeta):
*,
extra_params: dict[str, Any] | None = None,
) -> dict[str, Any]:
+ """
+ Perform a GraphQL query and return the data payload.
+
+ Args:
+ variables:
+ GraphQL query variables
+ extra_params:
+ Extra parameters to pass to `httpx.AsyncClient.post()`
+
+ Returns:
+ A dictionary of the query's `data` response payload
+
+ Raises:
+ sourcehut.exceptions.SrhtClientError:
+ The query returned a non-2XX return code and/or contained an
+ `error` responde payload
+ sourcehut.exceptions.ResourceNotFoundError:
+ A resource was not found
+ """
return await self.client.query(
self.SERVICE, query, variables, extra_params=extra_params
)
diff --git a/src/sourcehut/services/builds.py b/src/sourcehut/services/builds.py
index e9aecaa..f5994b2 100644
--- a/src/sourcehut/services/builds.py
+++ b/src/sourcehut/services/builds.py
@@ -201,6 +201,10 @@ class BuildsSrhtClient(_ServiceClient):
class JOB_STATUS(str, Enum):
+ """
+ String enum of possible job statuses
+ """
+
PENDING = "PENDING"
QUEUED = "QUEUED"
RUNNING = "RUNNING"
@@ -226,6 +230,10 @@ class JOB_STATUS(str, Enum):
class Job(_Resource[BuildsSrhtClient]):
+ """
+ Model representing a builds.sr.ht job. Do not instantiate directly!
+ """
+
created: DT
updated: DT
status: JOB_STATUS
@@ -259,14 +267,22 @@ class Job(_Resource[BuildsSrhtClient]):
class Secret(_Resource[BuildsSrhtClient]):
+ """
+ Model representing a builds.sr.ht secret. Do not instantiate directly!
+ """
+
created: DT
uuid: str
name: Optional[str]
class SecretFile(Secret):
+ """
+ Model representing a builds.sr.ht secret. Do not instantiate directly!
+ """
+
path: str
mode: int
-__all__ = ("BuildsSrhtClient", "JOB_STATUS", "Job")
+__all__ = ("BuildsSrhtClient", "JOB_STATUS", "Job", "Secret", "SecretFile")
diff --git a/src/sourcehut/services/git.py b/src/sourcehut/services/git.py
index f4de5f3..d03c5de 100644
--- a/src/sourcehut/services/git.py
+++ b/src/sourcehut/services/git.py
@@ -189,6 +189,20 @@ class GitSrhtClient(_ServiceClient):
filename: StrPath,
content: str | bytes | IO[bytes],
) -> Artifact:
+ """
+ Upload an artifact to a tag
+
+ Args:
+ repoid:
+ [`Repository`][sourcehut.services.git.Repository] object
+ or repository ID
+ revspec:
+ Reference to which artifact should be uploaded
+ filename:
+ Name of artifact file
+ content:
+ The context as a `str`, `bytes`, or a `bytes` IO stream
+ """
query = """
mutation upload($repoid: Int!, $revspec: String!, $file: Upload!) {
uploadArtifact(repoId: $repoid, revspec: $revspec, file: $file) {
@@ -223,6 +237,10 @@ class GitSrhtClient(_ServiceClient):
class Repository(_Resource[GitSrhtClient]):
+ """
+ Git repository model. Do not instantiate directly!
+ """
+
owner: str
name: str
visibility: VISIBILITY
@@ -266,6 +284,10 @@ class Repository(_Resource[GitSrhtClient]):
class Artifact(_Resource[GitSrhtClient]):
+ """
+ Git artifact model. Do not instantiate directly!
+ """
+
created: DT
filename: str
checksum: str
diff --git a/src/sourcehut/services/lists.py b/src/sourcehut/services/lists.py
index 5656e9e..a2474a5 100644
--- a/src/sourcehut/services/lists.py
+++ b/src/sourcehut/services/lists.py
@@ -45,6 +45,10 @@ else:
class ListsSrhtClient(_ServiceClient):
+ """
+ Client for lists.sr.ht
+ """
+
SERVICE = SRHT_SERVICE.LISTS
async def create_list(
@@ -161,6 +165,11 @@ class ListsSrhtClient(_ServiceClient):
class MailingListRef(_Resource[ListsSrhtClient]):
+ """
+ Lightweight model representing a reference to a lists.sr.ht mailing list
+ with methods to query and modify the list.
+ """
+
owner: str
name: str
_v_owner = v_submitter("owner")
@@ -180,6 +189,10 @@ class MailingListRef(_Resource[ListsSrhtClient]):
class MailingList(MailingListRef):
+ """
+ Full model representing a lists.sr.ht mailing list.
+ """
+
created: DT
updated: DT
description: Optional[str]
diff --git a/src/sourcehut/services/meta.py b/src/sourcehut/services/meta.py
index bbaa5f8..3382640 100644
--- a/src/sourcehut/services/meta.py
+++ b/src/sourcehut/services/meta.py
@@ -89,6 +89,10 @@ source
class MetaSrhtClient(_ServiceClient):
+ """
+ Client for meta.sr.ht
+ """
+
SERVICE = SRHT_SERVICE.META
async def create_pgp_key(self, key: str) -> PGPKey:
@@ -313,6 +317,11 @@ class MetaSrhtClient(_ServiceClient):
class PGPKeyRef(_Resource[MetaSrhtClient]):
+ """
+ Lightweight model representing a reference to a user's PGP key with methods
+ to query and modify the key.
+ """
+
user: str
fingerprint: str
_v_user = v_submitter("user")
@@ -328,6 +337,10 @@ class PGPKeyRef(_Resource[MetaSrhtClient]):
class PGPKey(PGPKeyRef):
+ """
+ Full model representing a PGP key.
+ """
+
created: DT
last_used: Optional[DT] = Field(alias="lastUsed")
key: str
@@ -335,6 +348,11 @@ class PGPKey(PGPKeyRef):
class SSHKeyRef(_Resource[MetaSrhtClient]):
+ """
+ Lightweight model representing a reference to a user's SHH key with methods
+ to query and modify the key.
+ """
+
user: str
fingerprint: str
_v_user = v_submitter("user")
@@ -350,6 +368,10 @@ class SSHKeyRef(_Resource[MetaSrhtClient]):
class SSHKey(SSHKeyRef):
+ """
+ Full model representing an SSH key
+ """
+
created: DT
last_used: Optional[DT] = Field(alias="lastUsed")
key: str
@@ -357,11 +379,19 @@ class SSHKey(SSHKeyRef):
class UserRef(_UserRef, _Resource[MetaSrhtClient]):
+ """
+ Lightweight model representing a reference to a meta.sr.ht user
+ """
+
async def get(self) -> User:
return await self._client.get_user(self)
class User(UserRef):
+ """
+ Full model of a meta.sr.ht user
+ """
+
created: DT
updated: DT
url: Optional[str]
@@ -370,6 +400,10 @@ class User(UserRef):
class AuditLogEntry(_Resource[MetaSrhtClient]):
+ """
+ Model of a meta.sr.ht audit log entry
+ """
+
created: DT
ip_address: str = Field(alias="ipAddress")
event_type: str = Field(alias="eventType")
@@ -377,6 +411,10 @@ class AuditLogEntry(_Resource[MetaSrhtClient]):
class Invoice(_Resource[MetaSrhtClient]):
+ """
+ Model of a meta.sr.ht invoice entry
+ """
+
created: DT
cents: int
valid_thru: DT = Field(alias="validThru")
diff --git a/src/sourcehut/services/pages.py b/src/sourcehut/services/pages.py
index 06f8695..74b7fff 100644
--- a/src/sourcehut/services/pages.py
+++ b/src/sourcehut/services/pages.py
@@ -39,6 +39,10 @@ notFound
class SITE_PROTOCOL(str, Enum):
+ """
+ String enum of supportred pages.sr.ht site protocols
+ """
+
HTTPS = "https"
GEMINI = "gemini"
@@ -48,6 +52,16 @@ class SITE_PROTOCOL(str, Enum):
@dataclass()
class FileConfig:
+ """
+ Represents file configurations used by the `PagesSrhtClient.publish()` method.
+
+ Attributes:
+ glob:
+ Glob of files to which this config should apply
+ cacheControl:
+ Value of the Cache-Control header to be used when serving the file
+ """
+
glob: str
cache_control: Optional[str] = None
@@ -81,6 +95,23 @@ class PagesSrhtClient(_ServiceClient):
not_found: Optional[str] = None,
file_configs: Optional[List[FileConfig]] = None,
) -> Site:
+ """
+ Publish a pages.sr.ht site
+
+ Args:
+ domain:
+ (Sub-)domain name
+ content:
+ `bytes` or `bytes` stream of a gziped tar archive of the site content
+ protocol:
+ Site protocol
+ subdirectory:
+ Optionally, publish the site to a subdirectory of `domain`
+ not_found:
+ Optionally, serve a custom 404 page
+ file_configs:
+ Optionally, specify configuration for specific files
+ """
inp = get_locals(**locals())
inp["siteConfig"] = _SiteConfig(
inp.pop("not_found"), inp.pop("file_configs")
@@ -160,6 +191,11 @@ class PagesSrhtClient(_ServiceClient):
class SiteRef(_Resource[PagesSrhtClient]):
+ """
+ Lightweight reference to a pages.sr.ht site with methods to query and
+ modify the site
+ """
+
domain: str
protocol: SITE_PROTOCOL
@@ -169,15 +205,19 @@ class SiteRef(_Resource[PagesSrhtClient]):
value = SITE_PROTOCOL.HTTPS
return value
+ async def unpublish(self) -> Site:
+ return await self._client.unpublish(self.domain, self.protocol)
+
class Site(SiteRef):
+ """
+ Full model of a pages.sr.ht site
+ """
+
created: DT
updated: DT
version: str
not_found: Optional[str] = Field(None, alias="notFound")
- async def unpublish(self) -> Site:
- return await self._client.unpublish(self.domain, self.protocol)
-
__all__ = ("SITE_PROTOCOL", "FileConfig", "SiteRef", "Site")
diff --git a/src/sourcehut/services/paste.py b/src/sourcehut/services/paste.py
index f8c56bd..9367f6a 100644
--- a/src/sourcehut/services/paste.py
+++ b/src/sourcehut/services/paste.py
@@ -39,11 +39,25 @@ files {
class PasteSrhtClient(_ServiceClient):
+ """
+ paste.sr.ht client
+ """
+
SERVICE = SRHT_SERVICE.PASTE
async def create(
self, files: Sequence[FileUpload], visibility: VISIBILITY = VISIBILITY.UNLISTED
) -> Paste:
+ """
+ Create a paste
+
+ Args:
+ files:
+ A list of [`FileUpload`][sourcehut.client.FileUpload] objects
+ to attach to the paste
+ visibility:
+ The paste's visibility
+ """
query = (
"""
mutation createPaste($files: [Upload!]!, $visibility: Visibility!) {
@@ -132,12 +146,21 @@ class PasteSrhtClient(_ServiceClient):
class File(_BaseResource[PasteSrhtClient]):
+ """
+ File in a paste.sr.ht paste
+ """
+
filename: Optional[str]
hash: str
contents: str
class PasteRef(_BaseResource[PasteSrhtClient]):
+ """
+ Lightweight model referencing a paste.sr.ht paste with methods to query and
+ modify the paste
+ """
+
id: str
user: str
_v_user = v_submitter("user")
@@ -153,6 +176,10 @@ class PasteRef(_BaseResource[PasteSrhtClient]):
class Paste(PasteRef):
+ """
+ Full model of a paste.sr.ht paste
+ """
+
created: DT
visibility: VISIBILITY
files: List[File]
diff --git a/src/sourcehut/services/todo.py b/src/sourcehut/services/todo.py
index b766726..6732098 100644
--- a/src/sourcehut/services/todo.py
+++ b/src/sourcehut/services/todo.py
@@ -70,6 +70,10 @@ entity {
class TodoSrhtClient(_ServiceClient):
+ """
+ todo.sr.ht client
+ """
+
SERVICE = SRHT_SERVICE.TODO
async def create_tracker(
@@ -677,6 +681,11 @@ class TodoSrhtClient(_ServiceClient):
class TrackerRef(_Resource[TodoSrhtClient]):
+ """
+ Lightweight model referencing a todo.sr.ht tracker with methods to query
+ and modify the tracker
+ """
+
name: str
owner: str
@@ -791,6 +800,10 @@ class TrackerRef(_Resource[TodoSrhtClient]):
class Tracker(TrackerRef):
+ """
+ Full model of a tracker
+ """
+
created: DT
updated: DT
description: Optional[str]
@@ -798,6 +811,10 @@ class Tracker(TrackerRef):
class TICKET_STATUS(str, Enum):
+ """
+ String enum of tracker ticket statuses
+ """
+
REPORTED = "REPORTED"
CONFIRMED = "CONFIRMED"
IN_PROGRESS = "IN_PROGRESS"
@@ -806,6 +823,10 @@ class TICKET_STATUS(str, Enum):
class TICKET_RESOLUTION(str, Enum):
+ """
+ String enum of tracker ticket resolutions
+ """
+
UNRESOLVED = "UNRESOLVED"
CLOSED = "CLOSED"
FIXED = "FIXED"
@@ -818,6 +839,11 @@ class TICKET_RESOLUTION(str, Enum):
class TicketRef(_Resource[TodoSrhtClient]):
+ """
+ Lightweight model referencing a tracker ticket with methods to query and
+ modify the ticket
+ """
+
tracker: TrackerRef
_v_client = v_client("tracker")
@@ -829,6 +855,10 @@ class TicketRef(_Resource[TodoSrhtClient]):
class Ticket(TicketRef):
+ """
+ Full model of a tracker ticket
+ """
+
created: DT
updated: DT
ref: str
@@ -855,6 +885,10 @@ class Comment:
class Label(_Resource[TodoSrhtClient]):
+ """
+ Model of a tracker label
+ """
+
created: DT
tracker: TrackerRef
name: str
--
2.43.0