Skip to content

Commit

Permalink
feat: list resource revisions for a package (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
lengau committed Dec 7, 2023
1 parent 0a8b8f2 commit 47e8458
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 0 deletions.
16 changes: 16 additions & 0 deletions craft_store/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from . import endpoints, errors, models
from .auth import Auth
from .http_client import HTTPClient
from .models.resource_revision_model import CharmResourceRevision
from .models.revisions_model import RevisionModel

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -298,6 +299,21 @@ def list_revisions(self, name: str) -> List[RevisionModel]:

return [RevisionModel.unmarshal(r) for r in response["revisions"]]

def list_resource_revisions(
self, name: str, resource_name: str
) -> List[CharmResourceRevision]:
"""List the revisions for a specific resource of a specific name."""
namespace = self._endpoints.namespace
if namespace != "charm":
raise NotImplementedError(
f"Cannot get resource revisions in namespace {namespace}."
)
endpoint = f"/v1/{namespace}/{name}/resources/{resource_name}/revisions"
response = self.request("GET", self._base_url + endpoint)
model = response.json()

return [CharmResourceRevision.unmarshal(r) for r in model["revisions"]]

def get_list_releases(self, *, name: str) -> models.MarshableModel:
"""Query the list_releases endpoint and return the result."""
endpoint = f"/v1/{self._endpoints.namespace}/{name}/releases"
Expand Down
55 changes: 55 additions & 0 deletions craft_store/models/resource_revision_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Resource revision response models for the Store."""
import datetime
from enum import Enum
from typing import List, Optional, Union

import pydantic

from craft_store.models._base_model import MarshableModel


class CharmResourceType(str, Enum):
"""Resource types for OCI images."""

OCI_IMAGE = "oci-image"
FILE = "file"


class CharmResourceBase(MarshableModel):
"""A base for a charm resource."""

name: str = "all"
channel: str = "all"
architectures: List[str] = ["all"]


class CharmResourceRevision(MarshableModel):
"""A basic resource revision."""

bases: List[CharmResourceBase]
created_at: datetime.datetime
name: str
revision: int
sha256: str
sha3_384: str
sha384: str
sha512: str
size: pydantic.ByteSize
type: Union[CharmResourceType, str]
updated_at: Optional[datetime.datetime] = None
updated_by: Optional[str] = None
62 changes: 62 additions & 0 deletions tests/integration/test_list_resource_revisions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Tests for list_releases."""
import datetime
from typing import cast

import pydantic
from craft_store.models.resource_revision_model import (
CharmResourceBase,
CharmResourceRevision,
CharmResourceType,
)

from .conftest import needs_charmhub_credentials


@needs_charmhub_credentials()
def test_charm_list_resource_revisions(charm_client, charmhub_charm_name):
revisions = charm_client.list_resource_revisions(charmhub_charm_name, "empty-file")

assert len(revisions) >= 1
assert isinstance(revisions[-1], CharmResourceRevision)

actual = cast(CharmResourceRevision, revisions[-1])

# Greater than or equal to in order to allow someone to replicate this
# integration test themselves.
assert actual.created_at >= datetime.datetime(
2023, 12, 1, tzinfo=datetime.timezone.utc
)
assert actual.revision >= 1

expected = CharmResourceRevision(
name="empty-file",
bases=[CharmResourceBase()],
type=CharmResourceType.FILE,
# These values are for an empty file.
size=pydantic.ByteSize(0),
sha256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
sha384="38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b",
sha3_384="0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004",
sha512="cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
# Copy the actual revision properties.
created_at=actual.created_at,
revision=actual.revision,
updated_at=actual.updated_at,
updated_by=actual.updated_by,
)
assert actual == expected
107 changes: 107 additions & 0 deletions tests/unit/test_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
import datetime
from unittest.mock import Mock

import pydantic
import pytest
import requests
from craft_store import BaseClient, endpoints
from craft_store.models import AccountModel, RegisteredNameModel
from craft_store.models._charm_model import CharmBaseModel
from craft_store.models._snap_models import Confinement, Grade, Type
from craft_store.models.resource_revision_model import (
CharmResourceBase,
CharmResourceRevision,
CharmResourceType,
)
from craft_store.models.revisions_model import (
CharmRevisionModel,
GitRevisionModel,
Expand Down Expand Up @@ -215,6 +221,107 @@ def test_list_revisions(charm_client, content, expected):
assert actual == expected


@pytest.mark.parametrize(
("content", "expected"),
[
pytest.param(b'{"revisions":[]}', [], id="empty"),
pytest.param(
b"""{"revisions":[{
"bases": [{"name": "all", "channel": "all", "architectures": ["all"]}],
"created-at": "1970-01-01T00:00:00",
"name": "resource",
"revision": 1,
"sha256": "a-sha256",
"sha3-384": "a 384-bit sha3",
"sha384": "a sha384",
"sha512": "a sha512",
"size": 17,
"type": "file",
"updated-at": "2020-03-14T00:00:00",
"updated-by": "lengau"
}]}""",
[
CharmResourceRevision(
bases=[CharmResourceBase()],
created_at=datetime.datetime(1970, 1, 1),
name="resource",
revision=1,
sha256="a-sha256",
sha3_384="a 384-bit sha3",
sha384="a sha384",
sha512="a sha512",
size=pydantic.ByteSize(17),
type=CharmResourceType.FILE,
updated_at=datetime.datetime(2020, 3, 14),
updated_by="lengau",
)
],
),
# Invalid data from the store that we should accept anyway.
pytest.param(
b"""{"revisions":[{
"bases": [
{"name": "all", "channel": "all", "architectures": ["all", "all"]},
{"name": "all", "channel": "all", "architectures": ["all", "all"]}
],
"created-at": "1970-01-01T00:00:00",
"name": "",
"revision": -1,
"sha256": "",
"sha3-384": "",
"sha384": "",
"sha512": "",
"size": 0,
"type": "invalid",
"updated-at": "2020-03-14T00:00:00",
"updated-by": ""
}]}""",
[
CharmResourceRevision(
bases=[
CharmResourceBase(architectures=["all", "all"]),
CharmResourceBase(architectures=["all", "all"]),
],
created_at=datetime.datetime(1970, 1, 1),
name="",
revision=-1,
sha256="",
sha3_384="",
sha384="",
sha512="",
size=pydantic.ByteSize(0),
type="invalid",
updated_at=datetime.datetime(2020, 3, 14),
updated_by="",
),
],
),
],
)
def test_list_resource_revisions_success(charm_client, content, expected):
charm_client.http_client.request.return_value = response = requests.Response()
response._content = content

actual = charm_client.list_resource_revisions("my-charm", "resource")

assert actual == expected


def test_list_resource_revisions_not_implemented():
"""list_resource_revisions is not implemented for non-charm namespaces."""
client = ConcreteTestClient(
base_url="https://staging.example.com",
storage_base_url="https://storage.staging.example.com",
endpoints=endpoints.SNAP_STORE,
application_name="testcraft",
user_agent="craft-store unit tests, should not be hitting a real server",
)
client.http_client = Mock(spec=client.http_client)

with pytest.raises(NotImplementedError):
client.list_resource_revisions("my-snap", "my-resource")


@pytest.mark.parametrize(
("name", "entity_type", "private", "team", "expected_json"),
[
Expand Down

0 comments on commit 47e8458

Please sign in to comment.