Maxwell G: 2 tests/unit: use fixtures instead of context managers BuildsSrhtClient: add list_secrets() 6 files changed, 152 insertions(+), 34 deletions(-)
sourcehutx/patches: SUCCESS in 5m50s [tests/unit: use fixtures instead of context managers][0] from [Maxwell G][1] [0]: https://lists.sr.ht/~gotmax23/sourcehutx/patches/46412 [1]: mailto:maxwell@gtmx.me ✓ #1088553 SUCCESS sourcehutx/patches/main.yml https://builds.sr.ht/~gotmax23/job/1088553 ✓ #1088554 SUCCESS sourcehutx/patches/mockbuild.yml https://builds.sr.ht/~gotmax23/job/1088554
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~gotmax23/sourcehutx/patches/46412/mbox | git am -3Learn more about email & git
--- tests/unit/test_builds.py | 34 ++++++++++++++++++---------------- tests/unit/test_client.py | 21 ++++++++++----------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/tests/unit/test_builds.py b/tests/unit/test_builds.py index 705992e..e7c6387 100644 --- a/tests/unit/test_builds.py +++ b/tests/unit/test_builds.py @@ -6,6 +6,7 @@ from __future__ import annotations from typing import Any import pytest +import pytest_asyncio import respx from sourcehut.client import SrhtClient @@ -26,24 +27,25 @@ JOB_DATA_1: dict[str, Any] = { } +@pytest_asyncio.fixture +async def client(fake_srht_client: SrhtClient) -> builds.BuildsSrhtClient: + return builds.BuildsSrhtClient(fake_srht_client) + + @pytest.mark.asyncio -async def test_builds_get_build(fake_client: SrhtClient): +async def test_builds_get_build(client: builds.BuildsSrhtClient): with respx.mock() as respx_mock: - async with fake_client: - client = builds.BuildsSrhtClient(fake_client) - endpoint = client.client.get_endpoint(client.SERVICE) - route = respx_mock.post(endpoint).respond( - json={"data": {"job": JOB_DATA_1}} - ) - gotten = await client.get_job(1039160) - assert gotten == builds.Job(**JOB_DATA_1, client=client) - assert route.call_count == 1 - second = await gotten.get() - assert second == gotten - assert route.call_count == 2 - status = await client.get_job_status(gotten) - assert status.succeeded - assert route.call_count == 3 + endpoint = client.client.get_endpoint(client.SERVICE) + route = respx_mock.post(endpoint).respond(json={"data": {"job": JOB_DATA_1}}) + gotten = await client.get_job(1039160) + assert gotten == builds.Job(**JOB_DATA_1, client=client) + assert route.call_count == 1 + second = await gotten.get() + assert second == gotten + assert route.call_count == 2 + status = await client.get_job_status(gotten) + assert status.succeeded + assert route.call_count == 3 def test_builds_job_status(): diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index acdc228..1d4b27a 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -38,19 +38,18 @@ def test_client_dunder(): @pytest.mark.asyncio -async def test_client_version(fake_client: SrhtClient): +async def test_client_version(fake_srht_client: SrhtClient): data = {"data": {"version": {"major": 1, "minor": 1, "patch": 0}}} - endpoint = fake_client.get_endpoint(SRHT_SERVICE.GIT) + endpoint = fake_srht_client.get_endpoint(SRHT_SERVICE.GIT) with respx.mock() as respx_mock: - async with fake_client: - route = respx_mock.post(endpoint).respond(json=data) - version = await fake_client.version(SRHT_SERVICE.GIT) - # Check that mock was used - assert route.call_count == 1 - # Check expected versions - assert version == APIVersion(1, 1, 0) - # Check __str__ - assert str(version) == "1.1.0" + route = respx_mock.post(endpoint).respond(json=data) + version = await fake_srht_client.version(SRHT_SERVICE.GIT) + # Check that mock was used + assert route.call_count == 1 + # Check expected versions + assert version == APIVersion(1, 1, 0) + # Check __str__ + assert str(version) == "1.1.0" @pytest.mark.parametrize( -- 2.41.0
Applied.
--- src/sourcehut/_utils.py | 16 +++++++- src/sourcehut/services/_base.py | 4 +- src/sourcehut/services/builds.py | 42 ++++++++++++++++++- tests/unit/test_builds.py | 69 +++++++++++++++++++++++++++++++- 4 files changed, 124 insertions(+), 7 deletions(-) diff --git a/src/sourcehut/_utils.py b/src/sourcehut/_utils.py index d21d5c5..be497ed 100644 --- a/src/sourcehut/_utils.py +++ b/src/sourcehut/_utils.py @@ -3,10 +3,10 @@ from __future__ import annotations -from collections.abc import Iterator, MutableMapping, Sequence +from collections.abc import Callable, Iterator, MutableMapping, Sequence from typing import TYPE_CHECKING, Any, TypeVar -from pydantic import validator +from pydantic import ValidationError, validator from .exceptions import ResourceNotFoundError @@ -93,3 +93,15 @@ def get_key(mapping, *keys: Sequence): if mapping is None: raise ResourceNotFoundError return mapping + + +def try_types(*types: type[_T]) -> Callable[..., _T]: + def inner(**__obj: Any) -> _T: + for typ in types: + try: + return typ(**__obj) + except ValidationError: + continue + raise TypeError(f"Failed to coerce object into {types}") + + return inner diff --git a/src/sourcehut/services/_base.py b/src/sourcehut/services/_base.py index 6513559..ff73c9d 100644 --- a/src/sourcehut/services/_base.py +++ b/src/sourcehut/services/_base.py @@ -8,7 +8,7 @@ Base classes for Sourcehut services from __future__ import annotations from abc import ABCMeta -from collections.abc import AsyncIterator, Sequence +from collections.abc import AsyncIterator, Callable, Sequence from typing import Any, Generic, TypeVar, Union from pydantic import BaseModel, Field @@ -65,7 +65,7 @@ class _ServiceClient(metaclass=ABCMeta): async def _cursorit( self, key: str | Sequence[str], - typ: type[_BaseResourceT], + typ: type[_BaseResourceT] | Callable[..., _BaseResourceT], query: str, max_pages: int | None, variables: dict[str, Any] | None = None, diff --git a/src/sourcehut/services/builds.py b/src/sourcehut/services/builds.py index 5a7d946..e9aecaa 100644 --- a/src/sourcehut/services/builds.py +++ b/src/sourcehut/services/builds.py @@ -13,7 +13,7 @@ from datetime import datetime as DT from enum import Enum from typing import TYPE_CHECKING, List, Optional -from .._utils import get_locals, v_submitter +from .._utils import get_locals, try_types, v_submitter from ..client import SRHT_SERVICE, VISIBILITY from ._base import _Resource, _ServiceClient @@ -35,6 +35,16 @@ owner { canonicalName } """ +_SECRET_MEMBERS = """ +id +created +uuid +name +... on SecretFile { + path + mode +} +""" class BuildsSrhtClient(_ServiceClient): @@ -170,6 +180,25 @@ class BuildsSrhtClient(_ServiceClient): ) return self._cursorit("jobs", Job, query, max_pages) + def list_secrets( + self, *, max_pages: int | None = 1 + ) -> AsyncIterator[SecretFile | Secret]: + query = ( + """ + query listSecrets($cursor: Cursor) { + secrets(cursor: $cursor) { + cursor + results { + %s + } + } + } """ + % _SECRET_MEMBERS + ) + return self._cursorit( + "secrets", try_types(SecretFile, Secret), query, max_pages + ) + class JOB_STATUS(str, Enum): PENDING = "PENDING" @@ -229,4 +258,15 @@ class Job(_Resource[BuildsSrhtClient]): return await self._client.get_job(self) +class Secret(_Resource[BuildsSrhtClient]): + created: DT + uuid: str + name: Optional[str] + + +class SecretFile(Secret): + path: str + mode: int + + __all__ = ("BuildsSrhtClient", "JOB_STATUS", "Job") diff --git a/tests/unit/test_builds.py b/tests/unit/test_builds.py index e7c6387..c1fa43b 100644 --- a/tests/unit/test_builds.py +++ b/tests/unit/test_builds.py @@ -3,6 +3,7 @@ from __future__ import annotations +import datetime from typing import Any import pytest @@ -12,10 +13,19 @@ import respx from sourcehut.client import SrhtClient from sourcehut.services import builds +FAKE_DATE_1 = "2023-08-11T06:00:02.050859Z" +FAKE_DATE_1_DT = datetime.datetime( + 2023, 8, 11, 6, 0, 2, 50859, tzinfo=datetime.timezone.utc +) +FAKE_DATE_2 = "2023-08-11T06:03:50.753334Z" +FAKE_DATE_2_DT = datetime.datetime( + 2023, 8, 11, 6, 3, 50, 753334, tzinfo=datetime.timezone.utc +) + JOB_DATA_1: dict[str, Any] = { "id": 1039160, - "created": "2023-08-11T06:00:02.050859Z", - "updated": "2023-08-11T06:03:50.753334Z", + "created": FAKE_DATE_1, + "updated": FAKE_DATE_2, "status": "SUCCESS", "manifest": "", "note": "", @@ -25,6 +35,31 @@ JOB_DATA_1: dict[str, Any] = { "runner": None, "owner": {"canonicalName": "~person"}, } +SECRET_DATA = {"id": 12345, "created": FAKE_DATE_1, "uuid": "12345", "name": "test key"} +SECRET_FILE_DATA = { + **SECRET_DATA, + "name": "test file", + "path": "abcd/123", + "mode": 0o600, +} + + +def SECRET_DATA_OBJ(**kwargs): + return builds.Secret( + id=12345, created=FAKE_DATE_1_DT, uuid="12345", name="test key", **kwargs + ) + + +def SECRET_FILE_OBJ(**kwargs): + return builds.SecretFile( + id=12345, + created=FAKE_DATE_1_DT, + uuid="12345", + name="test file", + path="abcd/123", + mode=0o600, + **kwargs, + ) @pytest_asyncio.fixture @@ -48,6 +83,36 @@ async def test_builds_get_build(client: builds.BuildsSrhtClient): assert route.call_count == 3 +@pytest.mark.asyncio +async def test_builds_list_secrets(client: builds.BuildsSrhtClient): + with respx.mock() as respx_mock: + endpoint = client.client.get_endpoint(client.SERVICE) + route = respx_mock.post(endpoint).respond( + json={ + "data": { + "secrets": { + "cursor": None, + "results": [ + SECRET_DATA, + SECRET_FILE_DATA, + SECRET_FILE_DATA, + SECRET_DATA, + ], + } + } + } + ) + results = [secret async for secret in client.list_secrets()] + expected = [ + SECRET_DATA_OBJ(client=client), + SECRET_FILE_OBJ(client=client), + SECRET_FILE_OBJ(client=client), + SECRET_DATA_OBJ(client=client), + ] + assert results == expected + assert route.call_count == 1 + + def test_builds_job_status(): pending = builds.JOB_STATUS.PENDING assert pending.in_progress -- 2.41.0
builds.sr.ht <builds@sr.ht>sourcehutx/patches: SUCCESS in 5m50s [tests/unit: use fixtures instead of context managers][0] from [Maxwell G][1] [0]: https://lists.sr.ht/~gotmax23/sourcehutx/patches/46412 [1]: mailto:maxwell@gtmx.me ✓ #1088553 SUCCESS sourcehutx/patches/main.yml https://builds.sr.ht/~gotmax23/job/1088553 ✓ #1088554 SUCCESS sourcehutx/patches/mockbuild.yml https://builds.sr.ht/~gotmax23/job/1088554