Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: list resource revisions for a package #129

Merged
merged 2 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
73 changes: 73 additions & 0 deletions craft_store/models/resource_revision_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2022 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 Optional

import pydantic

from craft_store.models._base_model import MarshableModel


class ArchitectureList(pydantic.ConstrainedList):
"""A list of architectures."""

__args__ = (str,)
item_type = str
min_items = 1
unique_items = True


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: ArchitectureList = ArchitectureList(["all"])


class CharmResourceBaseList(pydantic.ConstrainedList):
"""A list of charm resource bases."""

__args__ = (CharmResourceBase,)
item_type = CharmResourceBase
min_items = 1
unique_items = True


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

bases: CharmResourceBaseList
created_at: datetime.datetime
name: str
revision: int
sha256: str
sha3_384: str
sha384: str
sha512: str
size: pydantic.ByteSize
type: CharmResourceType
updated_at: Optional[datetime.datetime] = None
updated_by: Optional[str] = None
61 changes: 61 additions & 0 deletions tests/integration/test_list_resource_revisions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- 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,
CharmResourceBaseList,
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=CharmResourceBaseList([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,
)
assert actual == expected
69 changes: 69 additions & 0 deletions tests/unit/test_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@
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,
CharmResourceBaseList,
CharmResourceRevision,
CharmResourceType,
)
from craft_store.models.revisions_model import (
CharmRevisionModel,
GitRevisionModel,
Expand Down Expand Up @@ -215,6 +222,68 @@ 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=CharmResourceBaseList([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",
)
],
),
],
)
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