~gotmax23/sourcehutx

sourcehutx: tests/unit: use fixtures instead of context managers v1 APPLIED

Maxwell G: 2
 tests/unit: use fixtures instead of context managers
 BuildsSrhtClient: add list_secrets()

 6 files changed, 152 insertions(+), 34 deletions(-)
#1088553 main.yml success
#1088554 mockbuild.yml success
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
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH sourcehutx 1/2] tests/unit: use fixtures instead of context managers Export this patch

---
 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.

[PATCH sourcehutx 2/2] BuildsSrhtClient: add list_secrets() Export this patch

---
 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
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