From 5c5d2cc5f895fe78b756b40b1bd0004012412fdb Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Tue, 10 Dec 2024 10:38:36 -0300 Subject: [PATCH 1/3] refactor: add _StandardRecipe base class This class will hold the implementation that is valid for all non-snap recipe types. For now it's just the previous CharmRecipe - the next commit will do the generalization work. --- craft_application/launchpad/models/recipe.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/craft_application/launchpad/models/recipe.py b/craft_application/launchpad/models/recipe.py index 981be721..2aef9782 100644 --- a/craft_application/launchpad/models/recipe.py +++ b/craft_application/launchpad/models/recipe.py @@ -326,7 +326,7 @@ def build( return self._build(deadline, request_build_kwargs) -class CharmRecipe(_StoreRecipe): +class _StandardRecipe(_StoreRecipe): """A recipe for a charm. https://api.launchpad.net/devel.html#charm_recipe @@ -443,4 +443,11 @@ def build( return self._build(deadline, kwargs) +class CharmRecipe(_StandardRecipe): + """A recipe for a charm. + + https://api.launchpad.net/devel.html#charm_recipe + """ + + Recipe = SnapRecipe | CharmRecipe From 724c64849b5cce976ea708cf58bb4a898aeff43e Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Tue, 10 Dec 2024 11:08:21 -0300 Subject: [PATCH 2/3] refactor: generalize _StandardRecipe Subclasses must provide the name of the artifact (charm, rock, etc) and the launchpad object that wraps the utility to manipulate recipes. --- craft_application/launchpad/models/recipe.py | 57 ++++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/craft_application/launchpad/models/recipe.py b/craft_application/launchpad/models/recipe.py index 2aef9782..524e15d9 100644 --- a/craft_application/launchpad/models/recipe.py +++ b/craft_application/launchpad/models/recipe.py @@ -31,7 +31,7 @@ import enum import time from collections.abc import Collection, Iterable -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar, Literal import lazr.restfulclient.errors # type: ignore[import-untyped] from typing_extensions import Any, Self, TypedDict, override @@ -327,10 +327,15 @@ def build( class _StandardRecipe(_StoreRecipe): - """A recipe for a charm. + """A base class for recipes with common launchpad builds.""" - https://api.launchpad.net/devel.html#charm_recipe - """ + ARTIFACT: ClassVar[str] + """The artifact type this recipe creates: charm, rock, etc.""" + + @classmethod + def _lp_recipe(cls, lp: Launchpad) -> Any: # noqa: ANN401 (use of any) + """Get the launchpad utility to manipulate recipes.""" + raise NotImplementedError @classmethod def new( # noqa: PLR0913 @@ -349,16 +354,14 @@ def new( # noqa: PLR0913 store_channels: Collection[str] = ("latest/edge",), git_ref: str | None = None, ) -> Self: - """Create a new charm recipe. - - See: https://api.launchpad.net/devel.html#charm_recipes-new + """Create a new recipe. :param lp: The Launchpad client to use for this recipe. :param name: The recipe name :param owner: The username of the person or team who owns the recipe :param project: The name of the project to which this recipe should be attached. :param build_path: (Optional) The path to the directory containing - charmcraft.yaml (if it's not the root directory). + the project file (if it's not the root directory). :param git_ref: A link to a git repository and branch from which to build. Mutually exclusive with bzr_branch. :param auto_build: Whether to automatically build on pushes to the branch. @@ -366,10 +369,10 @@ def new( # noqa: PLR0913 :param auto_build_channels: (Optional) A dictionary of channels to use for snaps installed in the build environment. :param store_name: (Optional) The name in the store to which to upload this - charm. - :param store_channels: (Optional) The channels onto which to publish the charm + asset. + :param store_channels: (Optional) The channels onto which to publish the asset if uploaded. - :returns: The Charm recipe. + :returns: The recipe. """ kwargs: dict[str, Any] = {} if auto_build: @@ -383,10 +386,10 @@ def new( # noqa: PLR0913 ) cls._fill_repo_info(kwargs, git_ref=git_ref) - charm_entry = retry( - f"create charm recipe {name!r}", + created_entry = retry( + f"create {cls.ARTIFACT} recipe {name!r}", lazr.restfulclient.errors.BadRequest, - lp.lp.charm_recipes.new, + cls._lp_recipe(lp).new, name=name, owner=util.get_person_link(owner), project=f"/{project}", @@ -394,23 +397,23 @@ def new( # noqa: PLR0913 **kwargs, ) - if not charm_entry: - raise ValueError("Failed to create charm recipe") + if not created_entry: + raise ValueError(f"Failed to create {cls.ARTIFACT} recipe") - return cls(lp, charm_entry) + return cls(lp, created_entry) @classmethod def get( # pyright: ignore[reportIncompatibleMethodOverride] cls, lp: Launchpad, name: str, owner: str, project: str | None = None ) -> Self: - """Get a charm recipe.""" + """Get a recipe.""" try: return cls( lp, retry( - f"get charm recipe {name!r}", + f"get {cls.ARTIFACT} recipe {name!r}", lazr.restfulclient.errors.NotFound, - lp.lp.charm_recipes.getByName, + cls._lp_recipe(lp).getByName, name=name, owner=util.get_person_link(owner), project=f"/{project}", @@ -418,16 +421,16 @@ def get( # pyright: ignore[reportIncompatibleMethodOverride] ) except lazr.restfulclient.errors.NotFound: raise ValueError( - f"Could not find charm recipe {name!r} in project {project!r} with owner {owner!r}", + f"Could not find {cls.ARTIFACT} recipe {name!r} in project {project!r} with owner {owner!r}", ) from None @classmethod def find( # pyright: ignore[reportIncompatibleMethodOverride] cls, lp: Launchpad, owner: str, *, name: str = "" ) -> Iterable[Self]: - """Find a Charm recipe by the owner.""" + """Find a recipe by the owner.""" owner = util.get_person_link(owner) - lp_recipes = lp.lp.charm_recipes.findByOwner(owner=util.get_person_link(owner)) + lp_recipes = cls._lp_recipe(lp).findByOwner(owner=util.get_person_link(owner)) for recipe in lp_recipes: if name and recipe.name != name: continue @@ -449,5 +452,13 @@ class CharmRecipe(_StandardRecipe): https://api.launchpad.net/devel.html#charm_recipe """ + ARTIFACT: ClassVar[Literal["charm"]] = "charm" # type: ignore[reportIncompatibleVariableOverride] + + @override + @classmethod + def _lp_recipe(cls, lp: Launchpad) -> Any: + """https://api.launchpad.net/devel.html#charm_recipes.""" + return lp.lp.charm_recipes + Recipe = SnapRecipe | CharmRecipe From 8a606f1a5b217cc61775b62193f14f3f209b39f6 Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Tue, 10 Dec 2024 15:44:27 -0300 Subject: [PATCH 3/3] fixup! refactor: generalize _StandardRecipe --- craft_application/launchpad/models/recipe.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/craft_application/launchpad/models/recipe.py b/craft_application/launchpad/models/recipe.py index 524e15d9..7e3f70b0 100644 --- a/craft_application/launchpad/models/recipe.py +++ b/craft_application/launchpad/models/recipe.py @@ -30,6 +30,7 @@ import enum import time +from abc import abstractmethod from collections.abc import Collection, Iterable from typing import TYPE_CHECKING, ClassVar, Literal @@ -333,9 +334,10 @@ class _StandardRecipe(_StoreRecipe): """The artifact type this recipe creates: charm, rock, etc.""" @classmethod - def _lp_recipe(cls, lp: Launchpad) -> Any: # noqa: ANN401 (use of any) + @abstractmethod + def _get_lp_recipe(cls, lp: Launchpad) -> Any: # noqa: ANN401 (use of any) """Get the launchpad utility to manipulate recipes.""" - raise NotImplementedError + raise NotImplementedError("Must be implemented by subclass.") @classmethod def new( # noqa: PLR0913 @@ -389,7 +391,7 @@ def new( # noqa: PLR0913 created_entry = retry( f"create {cls.ARTIFACT} recipe {name!r}", lazr.restfulclient.errors.BadRequest, - cls._lp_recipe(lp).new, + cls._get_lp_recipe(lp).new, name=name, owner=util.get_person_link(owner), project=f"/{project}", @@ -413,7 +415,7 @@ def get( # pyright: ignore[reportIncompatibleMethodOverride] retry( f"get {cls.ARTIFACT} recipe {name!r}", lazr.restfulclient.errors.NotFound, - cls._lp_recipe(lp).getByName, + cls._get_lp_recipe(lp).getByName, name=name, owner=util.get_person_link(owner), project=f"/{project}", @@ -430,7 +432,9 @@ def find( # pyright: ignore[reportIncompatibleMethodOverride] ) -> Iterable[Self]: """Find a recipe by the owner.""" owner = util.get_person_link(owner) - lp_recipes = cls._lp_recipe(lp).findByOwner(owner=util.get_person_link(owner)) + lp_recipes = cls._get_lp_recipe(lp).findByOwner( + owner=util.get_person_link(owner) + ) for recipe in lp_recipes: if name and recipe.name != name: continue @@ -456,7 +460,7 @@ class CharmRecipe(_StandardRecipe): @override @classmethod - def _lp_recipe(cls, lp: Launchpad) -> Any: + def _get_lp_recipe(cls, lp: Launchpad) -> Any: """https://api.launchpad.net/devel.html#charm_recipes.""" return lp.lp.charm_recipes