diff --git a/charmcraft/commands/store/__init__.py b/charmcraft/commands/store/__init__.py index cf64f08f2..e095e78ad 100644 --- a/charmcraft/commands/store/__init__.py +++ b/charmcraft/commands/store/__init__.py @@ -16,6 +16,7 @@ """Commands related to Charmhub.""" import collections +import dataclasses import os import pathlib import shutil @@ -1552,7 +1553,7 @@ def run(self, parsed_args): if lib_data.lib_id is None: for tip in libs_tips.values(): if lib_data.charm_name == tip.charm_name and lib_data.lib_name == tip.lib_name: - lib_data = lib_data._replace(lib_id=tip.lib_id) + lib_data = dataclasses.replace(lib_data, lib_id=tip.lib_id) break tip = libs_tips.get((lib_data.lib_id, lib_data.api)) @@ -1604,7 +1605,8 @@ def run(self, parsed_args): # fix lib_data with new info so it's later available # for the case of programmatic output - lib_data = lib_data._replace( + lib_data = dataclasses.replace( + lib_data, patch=downloaded.patch, content=downloaded.content, content_hash=downloaded.content_hash, diff --git a/charmcraft/commands/store/charmlibs.py b/charmcraft/commands/store/charmlibs.py index 8332575aa..c2c1acbb0 100644 --- a/charmcraft/commands/store/charmlibs.py +++ b/charmcraft/commands/store/charmlibs.py @@ -20,7 +20,6 @@ import hashlib import os import pathlib -from collections import namedtuple from dataclasses import dataclass from typing import List, Optional, Set @@ -29,10 +28,20 @@ from charmcraft.errors import BadLibraryNameError, BadLibraryPathError -LibData = namedtuple( - "LibData", - "lib_id api patch content content_hash full_name path lib_name charm_name", -) + +@dataclass(frozen=True) +class LibData: + """All data fields for a library, including external ones.""" + + lib_id: Optional[str] + api: int + patch: int + content: Optional[str] + content_hash: Optional[str] + full_name: str + path: pathlib.Path + lib_name: str + charm_name: str @dataclass @@ -42,9 +51,9 @@ class LibInternals: lib_id: str api: int patch: int - pydeps: list + pydeps: List[str] content_hash: str - content: bytes + content: str def get_name_from_metadata() -> Optional[str]: diff --git a/charmcraft/commands/store/store.py b/charmcraft/commands/store/store.py index 2e0fe21e7..5e6046f4b 100644 --- a/charmcraft/commands/store/store.py +++ b/charmcraft/commands/store/store.py @@ -15,13 +15,13 @@ # For further info, check https://github.com/canonical/charmcraft """The Store API handling.""" - +import dataclasses +import datetime import os import platform import time -from collections import namedtuple from functools import wraps -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Literal, Optional, Tuple import craft_store from craft_cli import CraftError, emit @@ -35,24 +35,193 @@ Client, ) + # helpers to build responses from this layer -Account = namedtuple("Account", "name username id") -Package = namedtuple("Package", "id name type") -MacaroonInfo = namedtuple("MacaroonInfo", "account channels packages permissions") -Entity = namedtuple("Charm", "entity_type name private status publisher_display_name") -Uploaded = namedtuple("Uploaded", "ok status revision errors") +@dataclasses.dataclass(frozen=True) +class Account: + """Charmcraft-specific store account model. + + Deprecated in favour of implementation in craft-store. + """ + + name: str + username: str + id: str + + +@dataclasses.dataclass(frozen=True) +class Package: + """Charmcraft-specific store package model. + + Deprecated in favour of implementation in craft-store. + """ + + id: Optional[str] + name: str + type: Literal["charm", "bundle"] + + +@dataclasses.dataclass(frozen=True) +class MacaroonInfo: + """Charmcraft-specific macaroon information model. + + Deprecated in favour of implementation in craft-store. + """ + + account: Account + channels: Optional[List[str]] + packages: Optional[List[Package]] + permissions: List[str] + + +@dataclasses.dataclass(frozen=True) +class Entity: + """Charmcraft-specific store entity model. + + Deprecated in favour of implementation in craft-store. + """ + + entity_type: Literal["charm", "bundle"] + name: str + private: bool + status: str + publisher_display_name: str + + +@dataclasses.dataclass(frozen=True) +class Error: + """Charmcraft-specific store error model. + + Deprecated in favour of implementation in craft-store. + """ + + message: str + code: str + + +@dataclasses.dataclass(frozen=True) +class Uploaded: + """Charmcraft-specific store upload result model. + + Deprecated in favour of implementation in craft-store. + """ + + ok: bool + status: int + revision: int + errors: List[Error] + + # XXX Facundo 2020-07-23: Need to do a massive rename to call `revno` to the "revision as # the number" inside the "revision as the structure", this gets super confusing in the code with # time, and now it's the moment to do it (also in Release below!) -Revision = namedtuple("Revision", "revision version created_at status errors bases") -Error = namedtuple("Error", "message code") -Release = namedtuple("Release", "revision channel expires_at resources base") -Channel = namedtuple("Channel", "name fallback track risk branch") -Library = namedtuple("Library", "api content content_hash lib_id lib_name charm_name patch") -Resource = namedtuple("Resource", "name optional revision resource_type") -ResourceRevision = namedtuple("ResourceRevision", "revision created_at size") -RegistryCredentials = namedtuple("RegistryCredentials", "image_name username password") -Base = namedtuple("Base", "architecture channel name") +@dataclasses.dataclass(frozen=True) +class Base: + """Charmcraft-specific store object base model. + + Deprecated in favour of implementation in craft-store. + """ + + architecture: str + channel: str + name: str + + +@dataclasses.dataclass(frozen=True) +class Revision: + """Charmcraft-specific store name revision model. + + Deprecated in favour of implementation in craft-store. + """ + + revision: int + version: Optional + created_at: datetime.datetime + status: str + errors: List[Error] + bases: List[Base] + + +@dataclasses.dataclass(frozen=True) +class Resource: + """Charmcraft-specific store name resource model. + + Deprecated in favour of implementation in craft-store. + """ + + name: str + optional: bool + revision: int + resource_type: str + + +@dataclasses.dataclass(frozen=True) +class ResourceRevision: + """Charmcraft-specific store resource revision model. + + Deprecated in favour of implementation in craft-store. + """ + + revision: int + created_at: datetime.datetime + size: int + + +@dataclasses.dataclass(frozen=True) +class Release: + """Charmcraft-specific store release model. + + Deprecated in favour of implementation in craft-store. + """ + + revision: int + channel: str + expires_at: datetime.datetime + resources: List[Resource] + base: Base + + +@dataclasses.dataclass(frozen=True) +class Channel: + """Charmcraft-specific store channel model. + + Deprecated in favour of implementation in craft-store. + """ + + name: str + fallback: str + track: str + risk: str + branch: str + + +@dataclasses.dataclass(frozen=True) +class Library: + """Charmcraft-specific store library model. + + Deprecated in favour of implementation in craft-store. + """ + + lib_id: str + lib_name: str + charm_name: str + api: int + patch: int + content: Optional[str] + content_hash: str + + +@dataclasses.dataclass(frozen=True) +class RegistryCredentials: + """Charmcraft-specific store registry credential model. + + Deprecated in favour of implementation in craft-store. + """ + + image_name: str + username: str + password: str + # those statuses after upload that flag that the review ended (and if it ended successfully or not) UPLOAD_ENDING_STATUSES = {