---
fixtures/cassettes/test_base_get_user | 178 ++++++++++++++++++++++++++
fixtures/cassettes/test_get_user | 38 ++++++
src/sourcehut/services/_base.py | 39 ++++++
src/sourcehut/services/builds.py | 3 +
src/sourcehut/services/git.py | 14 +-
src/sourcehut/services/lists.py | 11 +-
src/sourcehut/services/meta.py | 25 ++--
src/sourcehut/services/pages.py | 3 +
src/sourcehut/services/todo.py | 29 ++++-
tests/integration/test_base.py | 49 +++++++
10 files changed, 367 insertions(+), 22 deletions(-)
create mode 100644 fixtures/cassettes/test_base_get_user
create mode 100644 fixtures/cassettes/test_get_user
create mode 100644 tests/integration/test_base.py
diff --git a/fixtures/cassettes/test_base_get_user b/fixtures/cassettes/test_base_get_user
new file mode 100644
index 0000000..35ac487
--- /dev/null
+++ b/fixtures/cassettes/test_base_get_user
@@ -0,0 +1,178 @@
+interactions:
+- request:
+ body: '{"query": "\n query getUserRef($username: String!) {\n user(username:
+ $username) {\n id\ncanonicalName\nusername\nemail\n\n }\n }\n ",
+ "variables": {"username": "gotmax23-test"}}'
+ headers:
+ accept:
+ - '*/*'
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '236'
+ content-type:
+ - application/json
+ host:
+ - git.sr.ht
+ user-agent:
+ - python-httpx/0.24.1
+ method: POST
+ uri: https://git.sr.ht/query
+ response:
+ content: '{"data":{"user":{"id":45032,"canonicalName":"~gotmax23-test","username":"gotmax23-test","email":"maxwell+client-test@gtmx.me"}}}'
+ headers:
+ Access-Control-Allow-Headers:
+ - User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range
+ Access-Control-Allow-Methods:
+ - GET, POST, OPTIONS
+ Access-Control-Allow-Origin:
+ - '*'
+ Access-Control-Expose-Headers:
+ - Content-Length,Content-Range
+ Connection:
+ - keep-alive
+ Content-Length:
+ - '128'
+ Content-Type:
+ - application/json
+ Date:
+ - Mon, 28 Aug 2023 02:42:57 GMT
+ Server:
+ - nginx
+ http_version: HTTP/1.1
+ status_code: 200
+- request:
+ body: '{"query": "\n query getUserRef($username: String!) {\n user(username:
+ $username) {\n id\ncanonicalName\nusername\nemail\n\n }\n }\n ",
+ "variables": {"username": "gotmax23-test"}}'
+ headers:
+ accept:
+ - '*/*'
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '236'
+ content-type:
+ - application/json
+ host:
+ - lists.sr.ht
+ user-agent:
+ - python-httpx/0.24.1
+ method: POST
+ uri: https://lists.sr.ht/query
+ response:
+ content: '{"data":{"user":{"id":45032,"canonicalName":"~gotmax23-test","username":"gotmax23-test","email":"maxwell+client-test@gtmx.me"}}}'
+ headers:
+ Access-Control-Allow-Headers:
+ - User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range
+ Access-Control-Allow-Methods:
+ - GET, POST, OPTIONS
+ Access-Control-Allow-Origin:
+ - '*'
+ Access-Control-Expose-Headers:
+ - Content-Length,Content-Range
+ Connection:
+ - keep-alive
+ Content-Length:
+ - '128'
+ Content-Type:
+ - application/json
+ Date:
+ - Mon, 28 Aug 2023 02:42:57 GMT
+ Server:
+ - nginx
+ http_version: HTTP/1.1
+ status_code: 200
+- request:
+ body: '{"query": "\n query getUserRef($username: String!) {\n userByName(username:
+ $username) {\n id\ncanonicalName\nusername\nemail\n\n }\n }\n ",
+ "variables": {"username": "gotmax23-test"}}'
+ headers:
+ accept:
+ - '*/*'
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '242'
+ content-type:
+ - application/json
+ host:
+ - meta.sr.ht
+ user-agent:
+ - python-httpx/0.24.1
+ method: POST
+ uri: https://meta.sr.ht/query
+ response:
+ content: '{"data":{"userByName":{"id":45032,"canonicalName":"~gotmax23-test","username":"gotmax23-test","email":"maxwell+client-test@gtmx.me"}}}'
+ headers:
+ Access-Control-Allow-Headers:
+ - User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range
+ Access-Control-Allow-Methods:
+ - GET, POST, OPTIONS
+ Access-Control-Allow-Origin:
+ - '*'
+ Access-Control-Expose-Headers:
+ - Content-Length,Content-Range
+ Connection:
+ - keep-alive
+ Content-Length:
+ - '134'
+ Content-Type:
+ - application/json
+ Date:
+ - Mon, 28 Aug 2023 02:42:57 GMT
+ Server:
+ - nginx
+ http_version: HTTP/1.1
+ status_code: 200
+- request:
+ body: '{"query": "\n query getUserRef($username: String!) {\n user(username:
+ $username) {\n id\ncanonicalName\nusername\nemail\n\n }\n }\n ",
+ "variables": {"username": "gotmax23-test"}}'
+ headers:
+ accept:
+ - '*/*'
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '236'
+ content-type:
+ - application/json
+ host:
+ - todo.sr.ht
+ user-agent:
+ - python-httpx/0.24.1
+ method: POST
+ uri: https://todo.sr.ht/query
+ response:
+ content: '{"data":{"user":{"id":45032,"canonicalName":"~gotmax23-test","username":"gotmax23-test","email":"maxwell+client-test@gtmx.me"}}}'
+ headers:
+ Access-Control-Allow-Headers:
+ - User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range
+ Access-Control-Allow-Methods:
+ - GET, POST, OPTIONS
+ Access-Control-Allow-Origin:
+ - '*'
+ Access-Control-Expose-Headers:
+ - Content-Length,Content-Range
+ Connection:
+ - keep-alive
+ Content-Length:
+ - '128'
+ Content-Type:
+ - application/json
+ Date:
+ - Mon, 28 Aug 2023 02:42:57 GMT
+ Server:
+ - nginx
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/fixtures/cassettes/test_get_user b/fixtures/cassettes/test_get_user
new file mode 100644
index 0000000..b641753
--- /dev/null
+++ b/fixtures/cassettes/test_get_user
@@ -0,0 +1,38 @@
+interactions:
+- request:
+ body: '{"query": "\n query getUserRef($username: String!) {\n user(username:
+ $username) {\n id\ncanonicalName\nusername\nemail\n\n }\n }\n ",
+ "variables": {"username": "gotmax23-test"}}'
+ headers:
+ accept:
+ - '*/*'
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '236'
+ content-type:
+ - application/json
+ host:
+ - builds.sr.ht
+ user-agent:
+ - python-httpx/0.24.1
+ method: POST
+ uri: https://builds.sr.ht/query
+ response:
+ content: '{"errors":[{"message":"Cannot query field \"user\" on type \"Query\".","locations":[{"line":3,"column":13}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}'
+ headers:
+ Connection:
+ - keep-alive
+ Content-Length:
+ - '172'
+ Content-Type:
+ - application/json
+ Date:
+ - Mon, 28 Aug 2023 01:00:38 GMT
+ Server:
+ - nginx
+ http_version: HTTP/1.1
+ status_code: 422
+version: 1
diff --git a/src/sourcehut/services/_base.py b/src/sourcehut/services/_base.py
index c7f2234..6513559 100644
--- a/src/sourcehut/services/_base.py
+++ b/src/sourcehut/services/_base.py
@@ -13,6 +13,7 @@ from typing import Any, Generic, TypeVar, Union
from pydantic import BaseModel, Field
+from .._utils import check_found as _cf
from .._utils import get_key as _get_key
from .._utils import infinite_iter
from ..client import SRHT_SERVICE, APIVersion, SrhtClient
@@ -20,6 +21,14 @@ from ..client import SRHT_SERVICE, APIVersion, SrhtClient
_ServiceClientT = TypeVar("_ServiceClientT", bound="_ServiceClient")
_BaseResourceT = TypeVar("_BaseResourceT", bound="_BaseResource")
_ResourceT = TypeVar("_ResourceT", bound="_Resource")
+_UserRefT = TypeVar("_UserRefT", bound="_UserRef")
+
+_USER_REF_MEMBERS = """\
+id
+canonicalName
+username
+email
+"""
class _ServiceClient(metaclass=ABCMeta):
@@ -93,3 +102,33 @@ class _Resource(Generic[_ServiceClientT], _BaseResource[_ServiceClientT]):
def __int__(self) -> int:
return self.id
+
+
+async def _get_user_ref(
+ client: _ServiceClient,
+ typ: type[_UserRefT],
+ username: Union[str, _UserRef, None],
+) -> _UserRefT:
+ username = await client._u(str(username) if username is not None else None)
+ query = (
+ """
+ query getUserRef($username: String!) {
+ user(username: $username) {
+ %s
+ }
+ }
+ """
+ % _USER_REF_MEMBERS
+ )
+ json = await client.query(query, {"username": username})
+ return typ(**_cf(json["user"]), client=client) # type: ignore[call-arg]
+
+
+class _UserRef(BaseModel):
+ id: int
+ username: str
+ email: str
+ canonical_name: str = Field(alias="canonicalName")
+
+ def __str__(self) -> str:
+ return self.username
diff --git a/src/sourcehut/services/builds.py b/src/sourcehut/services/builds.py
index b1e28c6..df67f67 100644
--- a/src/sourcehut/services/builds.py
+++ b/src/sourcehut/services/builds.py
@@ -232,3 +232,6 @@ class Job(_Resource[BuildsSrhtClient]):
Get the current state of the job
"""
return await self._client.get_job(self)
+
+
+__all__ = ("BuildsSrhtClient", "JOB_STATUS", "Job")
diff --git a/src/sourcehut/services/git.py b/src/sourcehut/services/git.py
index 978390f..f4de5f3 100644
--- a/src/sourcehut/services/git.py
+++ b/src/sourcehut/services/git.py
@@ -10,14 +10,14 @@ from __future__ import annotations
from collections.abc import AsyncIterator
from datetime import datetime as DT
-from typing import IO, TYPE_CHECKING, Optional
+from typing import IO, TYPE_CHECKING, Optional, Union
from .._utils import check_found as _cf
from .._utils import filter_ellipsis
from .._utils import get_key as _g
from .._utils import get_locals, v_submitter
from ..client import SRHT_SERVICE, VISIBILITY, _FileUpload
-from ._base import _Resource, _ServiceClient
+from ._base import _get_user_ref, _Resource, _ServiceClient, _UserRef
if TYPE_CHECKING:
from _typeshed import StrPath
@@ -218,6 +218,9 @@ class GitSrhtClient(_ServiceClient):
"""
await self.query(query, {"artifact": int(artifact)})
+ async def get_user_ref(self, username: Union[str, _UserRef, None]) -> UserRef:
+ return await _get_user_ref(self, UserRef, username)
+
class Repository(_Resource[GitSrhtClient]):
owner: str
@@ -271,3 +274,10 @@ class Artifact(_Resource[GitSrhtClient]):
async def delete(self) -> None:
return await self._client.delete_artifact(self)
+
+
+class UserRef(_UserRef, _Resource[GitSrhtClient]):
+ ...
+
+
+__all__ = ("GitSrhtClient", "Repository", "Artifact", "UserRef")
diff --git a/src/sourcehut/services/lists.py b/src/sourcehut/services/lists.py
index 0eb8eb4..5656e9e 100644
--- a/src/sourcehut/services/lists.py
+++ b/src/sourcehut/services/lists.py
@@ -14,7 +14,7 @@ from .._utils import filter_ellipsis
from .._utils import get_key as _g
from .._utils import get_locals, v_comma_separated_list, v_submitter
from ..client import SRHT_SERVICE, VISIBILITY
-from ._base import _Resource, _ServiceClient
+from ._base import _get_user_ref, _Resource, _ServiceClient, _UserRef
_LIST_REF_MEMBERS = """
id
@@ -156,6 +156,9 @@ class ListsSrhtClient(_ServiceClient):
data = await self.query(query, {"listid": listid, "inp": inp})
return MailingList(**_cf(data["updateMailingList"]), client=self)
+ async def get_user_ref(self, username: Union[str, _UserRef, None]) -> UserRef:
+ return await _get_user_ref(self, UserRef, username)
+
class MailingListRef(_Resource[ListsSrhtClient]):
owner: str
@@ -189,4 +192,8 @@ class MailingList(MailingListRef):
_v_reject_mime = v_comma_separated_list("reject_mime")
-__all__ = ("ListsSrhtClient", "MailingListRef", "MailingList")
+class UserRef(_UserRef, _Resource[ListsSrhtClient]):
+ ...
+
+
+__all__ = ("ListsSrhtClient", "MailingListRef", "MailingList", "UserRef")
diff --git a/src/sourcehut/services/meta.py b/src/sourcehut/services/meta.py
index 9379ea4..bbaa5f8 100644
--- a/src/sourcehut/services/meta.py
+++ b/src/sourcehut/services/meta.py
@@ -17,7 +17,7 @@ from pydantic import Field
from .._utils import check_found as _cf
from .._utils import get_locals, v_submitter
from ..client import SRHT_SERVICE
-from ._base import _Resource, _ServiceClient
+from ._base import _Resource, _ServiceClient, _UserRef
_PGPKey_REF_MEMBERS = """
id
@@ -356,24 +356,10 @@ class SSHKey(SSHKeyRef):
comment: Optional[str]
-class UserRef(_Resource[MetaSrhtClient]):
- username: str
- email: str
- canonical_name: str = Field(alias="canonicalName")
-
+class UserRef(_UserRef, _Resource[MetaSrhtClient]):
async def get(self) -> User:
return await self._client.get_user(self)
- def __str__(self) -> str:
- return self.username
-
-
-class AuditLogEntry(_Resource[MetaSrhtClient]):
- created: DT
- ip_address: str = Field(alias="ipAddress")
- event_type: str = Field(alias="eventType")
- details: Optional[str]
-
class User(UserRef):
created: DT
@@ -383,6 +369,13 @@ class User(UserRef):
bio: Optional[str]
+class AuditLogEntry(_Resource[MetaSrhtClient]):
+ created: DT
+ ip_address: str = Field(alias="ipAddress")
+ event_type: str = Field(alias="eventType")
+ details: Optional[str]
+
+
class Invoice(_Resource[MetaSrhtClient]):
created: DT
cents: int
diff --git a/src/sourcehut/services/pages.py b/src/sourcehut/services/pages.py
index 22eddef..06f8695 100644
--- a/src/sourcehut/services/pages.py
+++ b/src/sourcehut/services/pages.py
@@ -178,3 +178,6 @@ class Site(SiteRef):
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/todo.py b/src/sourcehut/services/todo.py
index e26df92..b766726 100644
--- a/src/sourcehut/services/todo.py
+++ b/src/sourcehut/services/todo.py
@@ -11,7 +11,7 @@ from __future__ import annotations
from collections.abc import AsyncIterator, Iterator
from datetime import datetime as DT
from enum import Enum
-from typing import TYPE_CHECKING, Optional, overload
+from typing import TYPE_CHECKING, Optional, Union, overload
from pydantic import BaseModel, Field
from pydantic.color import Color
@@ -21,7 +21,7 @@ from .._utils import filter_ellipsis
from .._utils import get_key as _g
from .._utils import get_locals, v_client, v_submitter
from ..client import SRHT_SERVICE, VISIBILITY
-from ._base import _BaseResource, _Resource, _ServiceClient
+from ._base import _BaseResource, _get_user_ref, _Resource, _ServiceClient, _UserRef
if TYPE_CHECKING:
pass
@@ -672,6 +672,9 @@ class TodoSrhtClient(_ServiceClient):
)
return Label(**_cf(tracker["label"]), tracker=tracker_ref)
+ async def get_user_ref(self, username: Union[str, _UserRef, None]) -> UserRef:
+ return await _get_user_ref(self, UserRef, username)
+
class TrackerRef(_Resource[TodoSrhtClient]):
name: str
@@ -912,3 +915,25 @@ class TrackerACLRef(_Resource[TodoSrhtClient]):
class TrackerACL(TrackerACLRef, TrackerACLBase):
created: DT
+
+
+class UserRef(_UserRef, _Resource[TodoSrhtClient]):
+ ...
+
+
+__all__ = (
+ "TodoSrhtClient",
+ "TrackerRef",
+ "Tracker",
+ "TICKET_STATUS",
+ "TICKET_RESOLUTION",
+ "TicketRef",
+ "Ticket",
+ "Comment",
+ "Label",
+ "TrackerSubscription",
+ "TrackerDefaultACL",
+ "TrackerACLRef",
+ "TrackerACL",
+ "UserRef",
+)
diff --git a/tests/integration/test_base.py b/tests/integration/test_base.py
new file mode 100644
index 0000000..c65c470
--- /dev/null
+++ b/tests/integration/test_base.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2023 Maxwell G <maxwell@gtmx.me>
+# SPDX-License-Identifier: MIT
+
+from __future__ import annotations
+
+import dataclasses
+
+import pytest
+
+from sourcehut.client import SrhtClient
+from sourcehut.services import _base, git, lists, meta, todo
+
+from .. import vcr
+
+
+@dataclasses.dataclass()
+class Service:
+ client_type: type[_base._ServiceClient]
+ user_ref_type: type[_base._UserRef]
+
+
+SERVICES: tuple[Service, ...] = (
+ Service(git.GitSrhtClient, git.UserRef),
+ Service(lists.ListsSrhtClient, lists.UserRef),
+ Service(meta.MetaSrhtClient, meta.UserRef),
+ Service(todo.TodoSrhtClient, todo.UserRef),
+)
+
+
+@pytest.mark.asyncio
+@vcr.use_cassette
+async def test_base_get_user(authed_client: SrhtClient):
+ async with authed_client:
+ for service in SERVICES:
+ client = service.client_type(authed_client)
+
+ user_ref = await client.get_user_ref( # type: ignore[attr-defined]
+ "gotmax23-test"
+ )
+ expected_ref = service.user_ref_type( # type: ignore[call-arg]
+ client=client,
+ username="gotmax23-test",
+ email="maxwell+client-test@gtmx.me",
+ canonicalName="~gotmax23-test",
+ id=user_ref.id,
+ )
+ assert user_ref.dict(include={"client"}) == expected_ref.dict(
+ include={"client"}
+ )
--
2.41.0