Skip to content

Commit

Permalink
feat: update resource revisions (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
lengau committed Dec 8, 2023
1 parent 317166c commit 3bb192b
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 30 deletions.
51 changes: 49 additions & 2 deletions craft_store/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import logging
from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, cast
from urllib.parse import urlparse

import requests
Expand All @@ -31,7 +31,11 @@
from . import endpoints, errors, models
from .auth import Auth
from .http_client import HTTPClient
from .models.resource_revision_model import CharmResourceRevision
from .models.resource_revision_model import (
CharmResourceRevision,
CharmResourceRevisionUpdateRequest,
RequestCharmResourceBaseList,
)
from .models.revisions_model import RevisionModel

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -314,6 +318,49 @@ def list_resource_revisions(

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

def update_resource_revisions(
self,
*updates: CharmResourceRevisionUpdateRequest,
name: str,
resource_name: str,
) -> int:
"""Update one or more resource revisions.
:param name: The package.
:param resource_name: The resource name to update.
:param *updates: The updates to make of any revisions
:returns: The number of revisions updated.
"""
if not updates:
raise ValueError("Need at least one resource revision to update.")
if (namespace := self._endpoints.namespace) != "charm":
raise NotImplementedError(
f"Cannot update resource revisions in namespace {namespace}."
)
endpoint = f"/v1/{namespace}/{name}/resources/{resource_name}/revisions"

body = {"resource-revision-updates": [update.marshal() for update in updates]}

response = self.request("PATCH", self._base_url + endpoint, json=body).json()

return cast(int, response["num-resource-revisions-updated"])

def update_resource_revision(
self,
name: str,
resource_name: str,
*,
revision: int,
bases: RequestCharmResourceBaseList,
) -> int:
"""Update a single resource revision."""
return self.update_resource_revisions(
CharmResourceRevisionUpdateRequest(revision=revision, bases=bases),
name=name,
resource_name=resource_name,
)

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
8 changes: 8 additions & 0 deletions craft_store/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
from .charm_list_releases_model import ListReleasesModel as CharmListReleasesModel
from .registered_name_model import RegisteredNameModel
from .release_request_model import ReleaseRequestModel
from .resource_revision_model import (
ResponseCharmResourceBase,
CharmResourceRevisionUpdateRequest,
RequestCharmResourceBase,
)
from .revisions_model import RevisionsRequestModel, RevisionsResponseModel
from .snap_list_releases_model import ListReleasesModel as SnapListReleasesModel
from .track_guardrail_model import TrackGuardrailModel
Expand All @@ -39,6 +44,9 @@
"MarshableModel",
"RegisteredNameModel",
"ReleaseRequestModel",
"ResponseCharmResourceBase",
"RequestCharmResourceBase",
"CharmResourceRevisionUpdateRequest",
"RevisionsRequestModel",
"RevisionsResponseModel",
"SnapListReleasesModel",
Expand Down
36 changes: 32 additions & 4 deletions craft_store/models/resource_revision_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@
#
# 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."""
"""Resource revision models for the Store."""
import datetime
from enum import Enum
from typing import List, Optional, Union
from typing import TYPE_CHECKING, List, Optional, Union

import pydantic

from craft_store.models._base_model import MarshableModel

if TYPE_CHECKING:
RequestArchitectureList = List[str]
else:
RequestArchitectureList = pydantic.conlist(
item_type=str, min_items=1, unique_items=True
)


class CharmResourceType(str, Enum):
"""Resource types for OCI images."""
Expand All @@ -30,7 +37,7 @@ class CharmResourceType(str, Enum):
FILE = "file"


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

name: str = "all"
Expand All @@ -41,7 +48,7 @@ class CharmResourceBase(MarshableModel):
class CharmResourceRevision(MarshableModel):
"""A basic resource revision."""

bases: List[CharmResourceBase]
bases: List[ResponseCharmResourceBase]
created_at: datetime.datetime
name: str
revision: int
Expand All @@ -53,3 +60,24 @@ class CharmResourceRevision(MarshableModel):
type: Union[CharmResourceType, str]
updated_at: Optional[datetime.datetime] = None
updated_by: Optional[str] = None


class RequestCharmResourceBase(ResponseCharmResourceBase):
"""A base for a charm resource for use in requests."""

architectures: RequestArchitectureList = ["all"]


if TYPE_CHECKING:
RequestCharmResourceBaseList = List[RequestCharmResourceBase]
else:
RequestCharmResourceBaseList = pydantic.conlist(
item_type=RequestCharmResourceBase, min_items=1
)


class CharmResourceRevisionUpdateRequest(MarshableModel):
"""A charm resource revision update request."""

revision: pydantic.PositiveInt
bases: RequestCharmResourceBaseList
34 changes: 14 additions & 20 deletions tests/integration/test_list_resource_revisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@
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
Expand All @@ -43,20 +40,17 @@ def test_charm_list_resource_revisions(charm_client, charmhub_charm_name):
)
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
sha256s = [r.sha256 for r in revisions]
sha384s = [r.sha384 for r in revisions]
sha3_384s = [r.sha3_384 for r in revisions]
sha512s = [r.sha512 for r in revisions]

expected_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
expected_sha384 = "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
expected_sha3_384 = "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004"
expected_sha512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"

assert expected_sha256 in sha256s
assert expected_sha384 in sha384s
assert expected_sha3_384 in sha3_384s
assert expected_sha512 in sha512s
68 changes: 68 additions & 0 deletions tests/integration/test_update_resource_revisions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- 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 update_resource_revisions."""

from craft_store.models.resource_revision_model import (
CharmResourceRevisionUpdateRequest,
RequestCharmResourceBase,
)

from .conftest import needs_charmhub_credentials


@needs_charmhub_credentials()
def test_charm_update_resource_revisions(charm_client, charmhub_charm_name):
resource_name = "empty-file"

revisions = charm_client.list_resource_revisions(charmhub_charm_name, resource_name)
revision_numbers = [r.revision for r in revisions[-2:]]
assert len(revision_numbers) == 2, "Needs more resource revisions"

resource_updates = [
CharmResourceRevisionUpdateRequest(
revision=revision_number,
bases=[
RequestCharmResourceBase(
name="ubuntu",
channel="22.04",
architectures=["riscv64"],
)
],
)
for revision_number in revision_numbers
]
default_updates = [
CharmResourceRevisionUpdateRequest(
revision=revision_number,
bases=[
RequestCharmResourceBase(
name="all", channel="all", architectures=["all"]
)
],
)
for revision_number in revision_numbers
]

update_count = charm_client.update_resource_revisions(
*resource_updates, name=charmhub_charm_name, resource_name="empty-file"
)
assert update_count == len(resource_updates)

# Reset back to the default.
update_count = charm_client.update_resource_revisions(
*default_updates, name=charmhub_charm_name, resource_name="empty-file"
)
assert update_count == len(default_updates)
42 changes: 42 additions & 0 deletions tests/unit/models/test_resource_revision_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- 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 resource revision models."""
import pydantic
import pytest
from craft_store.models import CharmResourceRevisionUpdateRequest


@pytest.mark.parametrize(
("request_dict", "match"),
[
({"revision": 1}, r"bases[:\s]+field required"),
(
{"revision": 1, "bases": []},
r"bases[:\s]+ensure this value has at least 1 item",
),
(
{"revision": 1, "bases": [{"architectures": ["all", "all"]}]},
r"bases -> 0 -> architectures[:\s]+the list has duplicated items",
),
(
{"revision": 1, "bases": [{"architectures": []}]},
r"bases -> 0 -> architectures[:\s]+ensure this value has at least 1 item",
),
],
)
def test_charmresourcerevisionupdaterequest_invalid_bases(request_dict, match):
with pytest.raises(pydantic.ValidationError, match=match):
CharmResourceRevisionUpdateRequest.unmarshal(request_dict)

0 comments on commit 3bb192b

Please sign in to comment.