diff --git a/python/copr/test/client_v3/test_projects.py b/python/copr/test/client_v3/test_projects.py new file mode 100644 index 000000000..379c16f25 --- /dev/null +++ b/python/copr/test/client_v3/test_projects.py @@ -0,0 +1,35 @@ +from copr.test import config_location, mock +from copr.v3 import Client +from copr.v3.proxies.project import UserPermissions + + +class TestProjectProxy(object): + @mock.patch("copr.v3.proxies.Request.send") + def test_add(self, send): + client = Client.create_from_config_file(config_location) + client.project_proxy.add("user1", "foo", ["fedora-rawhide-x86_64"]) + assert len(send.call_args_list) == 1 + call = send.call_args_list[0] + args = call[1] + assert args["method"] == "POST" + assert args["params"]["ownername"] == "user1" + assert args["data"]["name"] == "foo" + + @mock.patch("copr.v3.proxies.Request.send") + def test_set_permissions(self, send): + client = Client.create_from_config_file(config_location) + permissions: UserPermissions = { + "user1": { + "builder": "approved", + "admin": "nothing", + } + } + client.project_proxy.set_permissions("user1", "foo", permissions) + assert len(send.call_args_list) == 1 + call = send.call_args_list[0] + args = call[1] + assert args["method"] == "PUT" + assert args["data"]["user1"] == { + "admin": "nothing", + "builder": "approved", + } diff --git a/python/copr/v3/proxies/project.py b/python/copr/v3/proxies/project.py index 1c7c2bd7b..44739c25f 100644 --- a/python/copr/v3/proxies/project.py +++ b/python/copr/v3/proxies/project.py @@ -1,13 +1,60 @@ from __future__ import absolute_import import warnings +from typing import Any, Dict, List, Optional +from typing_extensions import Literal, TypedDict +from munch import Munch from . import BaseProxy from ..requests import munchify, DELETE, POST, PUT -from ..helpers import for_all_methods, bind_proxy +from ..helpers import for_all_methods, bind_proxy, List as CoprList -def _compat_use_bootstrap_container(data, value): +class PermissionRequest(TypedDict, total=False): + """ + Permissions that the authenticated user wants to request or drop. + """ + builder: bool + admin: bool + + +# Possible values that user can have or set for their `builder` and `admin` +# permissions on some project. +PermissionState = Literal["nothing", "request", "approved"] + + +class Permissions(TypedDict, total=False): + """ + A set of permissions that a user has or wants to have for some project + """ + builder: PermissionState + admin: PermissionState + + +# The `str` keys are usernames +UserPermissions = Dict[str, Permissions] + + +# Order items in ascending or descending order. +OrderType = Literal["ASC", "DESC"] + + +class PaginationMeta(TypedDict, total=False): + """ + How should the results be paginated? + More information here: + https://python-copr.readthedocs.io/en/latest/client_v3/pagination.html + """ + offset: int + limit: Optional[int] + order: str + order_type: OrderType + + +def _compat_use_bootstrap_container( + data: Dict[str, Any], + value: Optional[bool], +) -> None: if value is None: return data["bootstrap"] = "on" if value else "off" @@ -18,12 +65,12 @@ def _compat_use_bootstrap_container(data, value): @for_all_methods(bind_proxy) class ProjectProxy(BaseProxy): - def get(self, ownername, projectname): + def get(self, ownername: str, projectname: str) -> Munch: """ Return a project - :param str ownername: - :param str projectname: + :param ownername: + :param projectname: :return: Munch """ endpoint = "/project" @@ -34,13 +81,16 @@ def get(self, ownername, projectname): response = self.request.send(endpoint=endpoint, params=params) return munchify(response) - def get_list(self, ownername=None, pagination=None): + def get_list( + self, + ownername: Optional[str] = None, + pagination: Optional[PaginationMeta] = None, + ) -> CoprList: """ Return a list of projects - :param str ownername: + :param ownername: :param pagination: - :return: Munch """ endpoint = "/project/list" params = { @@ -50,13 +100,16 @@ def get_list(self, ownername=None, pagination=None): response = self.request.send(endpoint=endpoint, params=params) return munchify(response) - def search(self, query, pagination=None): + def search( + self, + query: str, + pagination: Optional[PaginationMeta] = None, + ) -> CoprList: """ Return a list of projects based on fulltext search - :param str query: + :param query: :param pagination: - :return: Munch """ endpoint = "/project/search" params = { @@ -66,51 +119,75 @@ def search(self, query, pagination=None): response = self.request.send(endpoint=endpoint, params=params) return munchify(response) - def add(self, ownername, projectname, chroots, description=None, instructions=None, homepage=None, - contact=None, additional_repos=None, unlisted_on_hp=False, enable_net=False, persistent=False, - auto_prune=True, use_bootstrap_container=None, devel_mode=False, - delete_after_days=None, multilib=False, module_hotfixes=False, - bootstrap=None, bootstrap_image=None, isolation=None, follow_fedora_branching=True, - fedora_review=None, appstream=False, runtime_dependencies=None, packit_forge_projects_allowed=None, - repo_priority=None, exist_ok=False, storage=None): + def add( + self, + ownername: str, + projectname: str, + chroots: List[str], + description: Optional[str] = None, + instructions: Optional[str] = None, + homepage: Optional[str] = None, + contact: Optional[str] = None, + additional_repos: Optional[List[str]] = None, + unlisted_on_hp: bool = False, + enable_net: bool = False, + persistent: bool = False, + auto_prune: bool = True, + use_bootstrap_container: Optional[bool] = None, + devel_mode: bool = False, + delete_after_days: Optional[int] = None, + multilib: bool = False, + module_hotfixes: bool = False, + bootstrap: Optional[str] = None, + bootstrap_image: Optional[str] = None, + isolation: Optional[str] = None, + follow_fedora_branching: bool = True, + fedora_review: Optional[bool] = None, + appstream: bool = False, + runtime_dependencies: Optional[List[str]] = None, + packit_forge_projects_allowed: Optional[List[str]] = None, + repo_priority: Optional[int] = None, + exist_ok: bool = False, + storage: Optional[str] = None, + ) -> Munch: """ Create a project - :param str ownername: - :param str projectname: - :param list chroots: - :param str description: - :param str instructions: - :param str homepage: - :param str contact: - :param list additional_repos: - :param bool unlisted_on_hp: project will not be shown on Copr homepage - :param bool enable_net: if builder can access net for builds in this project - :param bool persistent: if builds and the project are undeletable - :param bool auto_prune: if backend auto-deletion script should be run for the project - :param bool use_bootstrap_container: obsoleted, use the 'bootstrap' + :param ownername: + :param projectname: + :param chroots: + :param description: + :param instructions: + :param homepage: + :param contact: + :param additional_repos: + :param unlisted_on_hp: project will not be shown on Copr homepage + :param enable_net: if builder can access net for builds in this project + :param persistent: if builds and the project are undeletable + :param auto_prune: if backend auto-deletion script should be run for the project + :param use_bootstrap_container: obsoleted, use the 'bootstrap' argument and/or the 'bootstrap_image'. - :param bool devel_mode: if createrepo should run automatically - :param int delete_after_days: delete the project after the specfied period of time - :param bool module_hotfixes: allow packages from this project to + :param devel_mode: if createrepo should run automatically + :param delete_after_days: delete the project after the specfied period of time + :param module_hotfixes: allow packages from this project to override packages from active module streams. - :param str bootstrap: Mock bootstrap feature setup. + :param bootstrap: Mock bootstrap feature setup. Possible values are 'default', 'on', 'off', 'image'. - :param str bootstrap_image: Name of the container image to initialize + :param bootstrap_image: Name of the container image to initialize the bootstrap chroot from. This also implies 'bootstrap=image'. This is a noop parameter and its value is ignored. - :param str isolation: Mock isolation feature setup. + :param isolation: Mock isolation feature setup. Possible values are 'default', 'simple', 'nspawn'. - :param bool follow_fedora_branching: If newly branched chroots should be automatically enabled and populated - :param bool fedora_review: Run fedora-review tool for packages + :param follow_fedora_branching: If newly branched chroots should be automatically enabled and populated + :param fedora_review: Run fedora-review tool for packages in this project - :param bool appstream: Disable or enable generating the appstream metadata - :param string runtime_dependencies: List of external repositories + :param appstream: Disable or enable generating the appstream metadata + :param runtime_dependencies: List of external repositories (== dependencies, specified as baseurls) that will be automatically enabled together with this project repository. - :param list packit_forge_projects_allowed: List of forge projects that + :param packit_forge_projects_allowed: List of forge projects that will be allowed to build in the project via Packit - :param str storage: Admin only - What storage should be used for this project + :param storage: Admin only - What storage should be used for this project :return: Munch """ endpoint = "/project/add/{ownername}" @@ -157,48 +234,69 @@ def add(self, ownername, projectname, chroots, description=None, instructions=No ) return munchify(response) - def edit(self, ownername, projectname, chroots=None, description=None, instructions=None, homepage=None, - contact=None, additional_repos=None, unlisted_on_hp=None, enable_net=None, - auto_prune=None, use_bootstrap_container=None, devel_mode=None, - delete_after_days=None, multilib=None, module_hotfixes=None, - bootstrap=None, bootstrap_image=None, isolation=None, follow_fedora_branching=None, - fedora_review=None, appstream=None, runtime_dependencies=None, packit_forge_projects_allowed=None, - repo_priority=None): + def edit( + self, + ownername: str, + projectname: str, + chroots: Optional[List[str]] = None, + description: Optional[str] = None, + instructions: Optional[str] = None, + homepage: Optional[str] = None, + contact: Optional[str] = None, + additional_repos: Optional[List[str]] = None, + unlisted_on_hp: Optional[bool] = None, + enable_net: Optional[bool] = None, + auto_prune: Optional[bool] = None, + use_bootstrap_container: Optional[bool] = None, + devel_mode: Optional[bool] = None, + delete_after_days: Optional[int] = None, + multilib: Optional[bool] = None, + module_hotfixes: Optional[bool] = None, + bootstrap: Optional[str] = None, + bootstrap_image: Optional[str] = None, + isolation: Optional[str] = None, + follow_fedora_branching: Optional[bool] = None, + fedora_review: Optional[bool] = None, + appstream: Optional[bool] = None, + runtime_dependencies: Optional[List[str]] = None, + packit_forge_projects_allowed: Optional[List[str]] = None, + repo_priority: Optional[int] = None, + ) -> Munch: """ Edit a project - :param str ownername: - :param str projectname: - :param list chroots: - :param str description: - :param str instructions: - :param str homepage: - :param str contact: - :param list repos: - :param bool unlisted_on_hp: project will not be shown on Copr homepage - :param bool enable_net: if builder can access net for builds in this project - :param bool auto_prune: if backend auto-deletion script should be run for the project - :param bool use_bootstrap_container: obsoleted, use the 'bootstrap' + :param ownername: + :param projectname: + :param chroots: + :param description: + :param instructions: + :param homepage: + :param contact: + :param repos: + :param unlisted_on_hp: project will not be shown on Copr homepage + :param enable_net: if builder can access net for builds in this project + :param auto_prune: if backend auto-deletion script should be run for the project + :param use_bootstrap_container: obsoleted, use the 'bootstrap' argument and/or the 'bootstrap_image'. - :param bool devel_mode: if createrepo should run automatically - :param int delete_after_days: delete the project after the specfied period of time - :param bool module_hotfixes: allow packages from this project to + :param devel_mode: if createrepo should run automatically + :param delete_after_days: delete the project after the specfied period of time + :param module_hotfixes: allow packages from this project to override packages from active module streams. - :param str bootstrap: Mock bootstrap feature setup. + :param bootstrap: Mock bootstrap feature setup. Possible values are 'default', 'on', 'off', 'image'. - :param str isolation: Mock isolation feature setup. + :param isolation: Mock isolation feature setup. Possible values are 'default', 'simple', 'nspawn'. - :param bool follow_fedora_branching: If newly branched chroots should be automatically enabled and populated. - :param str bootstrap_image: Name of the container image to initialize + :param follow_fedora_branching: If newly branched chroots should be automatically enabled and populated. + :param bootstrap_image: Name of the container image to initialize the bootstrap chroot from. This also implies 'bootstrap=image'. This is a noop parameter and its value is ignored. - :param bool fedora_review: Run fedora-review tool for packages + :param fedora_review: Run fedora-review tool for packages in this project - :param bool appstream: Disable or enable generating the appstream metadata - :param string runtime_dependencies: List of external repositories + :param appstream: Disable or enable generating the appstream metadata + :param runtime_dependencies: List of external repositories (== dependencies, specified as baseurls) that will be automatically enabled together with this project repository. - :param list packit_forge_projects_allowed: List of forge projects that + :param packit_forge_projects_allowed: List of forge projects that will be allowed to build in the project via Packit :return: Munch """ @@ -243,12 +341,12 @@ def edit(self, ownername, projectname, chroots=None, description=None, instructi ) return munchify(response) - def delete(self, ownername, projectname): + def delete(self, ownername: str, projectname: str) -> Munch: """ Delete a project - :param str ownername: - :param str projectname: + :param ownername: + :param projectname: :return: Munch """ endpoint = "/project/delete/{ownername}/{projectname}" @@ -268,15 +366,22 @@ def delete(self, ownername, projectname): ) return munchify(response) - def fork(self, ownername, projectname, dstownername, dstprojectname, confirm=False): + def fork( + self, + ownername: str, + projectname: str, + dstownername: str, + dstprojectname: str, + confirm: bool = False, + ) -> Munch: """ Fork a project - :param str ownername: owner of a source project - :param str projectname: name of a source project - :param str dstownername: owner of a destination project - :param str dstprojectname: name of a destination project - :param bool confirm: if forking into a existing project, this needs to be set to True, + :param ownername: owner of a source project + :param projectname: name of a source project + :param dstownername: owner of a destination project + :param dstprojectname: name of a destination project + :param confirm: if forking into a existing project, this needs to be set to True, to confirm that user is aware of that :return: Munch """ @@ -299,14 +404,14 @@ def fork(self, ownername, projectname, dstownername, dstprojectname, confirm=Fal ) return munchify(response) - def can_build_in(self, who, ownername, projectname): + def can_build_in(self, who: str, ownername: str, projectname: str) -> bool: """ Return ``True`` a user can submit builds for a ownername/projectname - :param str who: name of the user checking their permissions - :param str ownername: owner of the project - :param str projectname: name of the project - :return Bool: ``True`` or raise + :param who: name of the user checking their permissions + :param ownername: owner of the project + :param projectname: name of the project + :return: ``True`` or raise """ endpoint = ("/project/permissions/can_build_in/" "{who}/{ownername}/{projectname}/") @@ -322,13 +427,13 @@ def can_build_in(self, who, ownername, projectname): ) return munchify(response).can_build_in - def get_permissions(self, ownername, projectname): + def get_permissions(self, ownername: str, projectname: str) -> Munch: """ Get project permissions - :param str ownername: owner of the project - :param str projectname: name of the project - :return Munch: a dictionary in format + :param ownername: owner of the project + :param projectname: name of the project + :return: a dictionary in format ``{username: {permission: state, ... }, ...}`` where ``username`` identifies an existing copr user, ``permission`` is one of ``admin|builder`` and state is one of ``nothing|approved|request``. @@ -343,13 +448,18 @@ def get_permissions(self, ownername, projectname): endpoint=endpoint, params=params, auth=self.auth) return munchify(response) - def set_permissions(self, ownername, projectname, permissions): + def set_permissions( + self, + ownername: str, + projectname: str, + permissions: UserPermissions, + ) -> None: """ Set (or change) permissions for a project - :param str ownername: owner of the updated project - :param str projectname: name of the updated project - :param dict permissions: the expected format is + :param ownername: owner of the updated project + :param projectname: name of the updated project + :param permissions: the expected format is ``{username: {permission: state, ...}, ...}`` where ``username`` identifies an existing copr user, ``permission`` is one of ``builder|admin`` and ``state`` value is one of @@ -376,13 +486,18 @@ def set_permissions(self, ownername, projectname, permissions): auth=self.auth, ) - def request_permissions(self, ownername, projectname, permissions): + def request_permissions( + self, + ownername: str, + projectname: str, + permissions: PermissionRequest, + ) -> None: """ Request/cancel request/drop your permissions on project - :param str ownername: owner of the requested project - :param str projectname: name of the requested project - :param dict permissions: the desired permissions user wants to have on + :param ownername: owner of the requested project + :param projectname: name of the requested project + :param permissions: the desired permissions user wants to have on the requested copr project. The format is ``{permission: bool, ...}``, where ``permission`` is one of ``builder|admin`` and ``bool`` is @@ -403,12 +518,12 @@ def request_permissions(self, ownername, projectname, permissions): auth=self.auth, ) - def regenerate_repos(self, ownername, projectname): + def regenerate_repos(self, ownername: str, projectname: str) -> Munch: """ Regenerate repositories for a project - :param str ownername: owner of the project to regenerate - :param str projectname: name of the project to regenerate + :param ownername: owner of the project to regenerate + :param projectname: name of the project to regenerate """ endpoint = "/project/regenerate-repos/{ownername}/{projectname}" params = { diff --git a/python/python-copr.spec b/python/python-copr.spec index 54db8b448..92c75c633 100644 --- a/python/python-copr.spec +++ b/python/python-copr.spec @@ -47,6 +47,7 @@ BuildRequires: python3-requests BuildRequires: python3-requests-toolbelt BuildRequires: python3-sphinx BuildRequires: python3-requests-gssapi +BuildRequires: python3-typing-extensions Requires: python3-munch Requires: python3-filelock @@ -54,6 +55,7 @@ Requires: python3-requests Requires: python3-requests-toolbelt Requires: python3-setuptools Requires: python3-requests-gssapi +Requires: python3-typing-extensions %{?python_provide:%python_provide python3-copr} diff --git a/python/setup.py b/python/setup.py index a177b1eca..6a09ff19b 100644 --- a/python/setup.py +++ b/python/setup.py @@ -19,6 +19,7 @@ 'requests-toolbelt', 'setuptools', 'munch', + 'typing_extensions>=3.7.4', ] __description__ = "Python client for copr service."