From 791de22f3d31813a1d2760a2e5d3323dd10849e9 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Tue, 18 Feb 2020 20:49:50 +0300 Subject: [PATCH 01/15] feat: replace requests with httpx Make all functions async Use httpx instead of httpx Make decorators async to correctly wrap async methods --- gitlab/__init__.py | 174 ++++++++------- gitlab/cli.py | 4 +- gitlab/exceptions.py | 4 +- gitlab/mixins.py | 93 ++++---- gitlab/v4/objects.py | 490 +++++++++++++++++++++--------------------- requirements.txt | 2 +- setup.py | 5 +- test-requirements.txt | 1 + 8 files changed, 389 insertions(+), 384 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 9a3a8b1d2..a2a860e67 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -16,18 +16,17 @@ # along with this program. If not, see . """Wrapper for the GitLab API.""" -from __future__ import print_function -from __future__ import absolute_import +from __future__ import absolute_import, print_function + +import asyncio import importlib -import time import warnings -import requests - import gitlab.config +import httpx +from gitlab import utils # noqa from gitlab.const import * # noqa from gitlab.exceptions import * # noqa -from gitlab import utils # noqa __title__ = "python-gitlab" __version__ = "2.0.1" @@ -82,7 +81,7 @@ def __init__( http_password=None, timeout=None, api_version="4", - session=None, + client=None, per_page=None, pagination=None, order_by=None, @@ -107,8 +106,7 @@ def __init__( self.job_token = job_token self._set_auth_info() - #: Create a session object for requests - self.session = session or requests.Session() + self.client = client or self._get_client() self.per_page = per_page self.pagination = pagination @@ -145,11 +143,25 @@ def __init__( self.pagesdomains = objects.PagesDomainManager(self) self.user_activities = objects.UserActivitiesManager(self) - def __enter__(self): + async def __aenter__(self): return self - def __exit__(self, *args): - self.session.close() + async def __aexit__(self, *args): + await self.client.aclose() + + def _get_client(self) -> httpx.AsyncClient: + if (self.http_username and not self.http_password) or ( + not self.http_username and self.http_password + ): + raise ValueError("Both http_username and http_password should be defined") + + auth = None + if self.http_username: + auth = httpx.auth.BasicAuth(self.http_username, self.http_password) + + return httpx.AsyncClient( + auth=auth, verify=self.ssl_verify, timeout=self.timeout, + ) def __getstate__(self): state = self.__dict__.copy() @@ -238,7 +250,7 @@ def version(self): return self._server_version, self._server_revision @on_http_error(GitlabVerifyError) - def lint(self, content, **kwargs): + async def lint(self, content, **kwargs): """Validate a gitlab CI configuration. Args: @@ -254,11 +266,11 @@ def lint(self, content, **kwargs): otherwise """ post_data = {"content": content} - data = self.http_post("/ci/lint", post_data=post_data, **kwargs) + data = await self.http_post("/ci/lint", post_data=post_data, **kwargs) return (data["status"] == "valid", data["errors"]) @on_http_error(GitlabMarkdownError) - def markdown(self, text, gfm=False, project=None, **kwargs): + async def markdown(self, text, gfm=False, project=None, **kwargs): """Render an arbitrary Markdown document. Args: @@ -279,11 +291,11 @@ def markdown(self, text, gfm=False, project=None, **kwargs): post_data = {"text": text, "gfm": gfm} if project is not None: post_data["project"] = project - data = self.http_post("/markdown", post_data=post_data, **kwargs) + data = await self.http_post("/markdown", post_data=post_data, **kwargs) return data["html"] @on_http_error(GitlabLicenseError) - def get_license(self, **kwargs): + async def get_license(self, **kwargs): """Retrieve information about the current license. Args: @@ -296,10 +308,10 @@ def get_license(self, **kwargs): Returns: dict: The current license information """ - return self.http_get("/license", **kwargs) + return await self.http_get("/license", **kwargs) @on_http_error(GitlabLicenseError) - def set_license(self, license, **kwargs): + async def set_license(self, license, **kwargs): """Add a new license. Args: @@ -314,7 +326,7 @@ def set_license(self, license, **kwargs): dict: The new license information """ data = {"license": license} - return self.http_post("/license", post_data=data, **kwargs) + return await self.http_post("/license", post_data=data, **kwargs) def _construct_url(self, id_, obj, parameters, action=None): if "next_url" in parameters: @@ -345,19 +357,12 @@ def _set_auth_info(self): "Only one of private_token, oauth_token or job_token should " "be defined" ) - if (self.http_username and not self.http_password) or ( - not self.http_username and self.http_password - ): - raise ValueError( - "Both http_username and http_password should " "be defined" - ) if self.oauth_token and self.http_username: raise ValueError( "Only one of oauth authentication or http " "authentication should be defined" ) - self._http_auth = None if self.private_token: self.headers.pop("Authorization", None) self.headers["PRIVATE-TOKEN"] = self.private_token @@ -373,11 +378,6 @@ def _set_auth_info(self): self.headers.pop("PRIVATE-TOKEN", None) self.headers["JOB-TOKEN"] = self.job_token - if self.http_username: - self._http_auth = requests.auth.HTTPBasicAuth( - self.http_username, self.http_password - ) - def enable_debug(self): import logging @@ -389,7 +389,7 @@ def enable_debug(self): HTTPConnection.debuglevel = 1 logging.basicConfig() logging.getLogger().setLevel(logging.DEBUG) - requests_log = logging.getLogger("requests.packages.urllib3") + requests_log = logging.getLogger("httpx") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True @@ -399,14 +399,6 @@ def _create_headers(self, content_type=None): request_headers["Content-type"] = content_type return request_headers - def _get_session_opts(self, content_type): - return { - "headers": self._create_headers(content_type), - "auth": self._http_auth, - "timeout": self.timeout, - "verify": self.ssl_verify, - } - def _build_url(self, path): """Returns the full url from path. @@ -441,7 +433,7 @@ def _check_redirects(self, result): if location and location.startswith("https://"): raise RedirectError(REDIRECT_MSG) - def http_request( + async def http_request( self, verb, path, @@ -491,12 +483,10 @@ def http_request( else: utils.copy_dict(params, kwargs) - opts = self._get_session_opts(content_type="application/json") + opts = {"headers": self._create_headers("application/json")} - verify = opts.pop("verify") - timeout = opts.pop("timeout") # If timeout was passed into kwargs, allow it to override the default - timeout = kwargs.get("timeout", timeout) + timeout = kwargs.get("timeout") # We need to deal with json vs. data when uploading files if files: @@ -507,20 +497,9 @@ def http_request( json = post_data data = None - # Requests assumes that `.` should not be encoded as %2E and will make - # changes to urls using this encoding. Using a prepped request we can - # get the desired behavior. - # The Requests behavior is right but it seems that web servers don't - # always agree with this decision (this is the case with a default - # gitlab installation) - req = requests.Request( + req = httpx.Request( verb, url, json=json, data=data, params=params, files=files, **opts ) - prepped = self.session.prepare_request(req) - prepped.url = utils.sanitized_url(prepped.url) - settings = self.session.merge_environment_settings( - prepped.url, {}, streamed, verify, None - ) # obey the rate limit by default obey_rate_limit = kwargs.get("obey_rate_limit", True) @@ -532,7 +511,7 @@ def http_request( cur_retries = 0 while True: - result = self.session.send(prepped, timeout=timeout, **settings) + result = await self.client.send(req, stream=streamed, timeout=timeout) self._check_redirects(result) @@ -547,7 +526,7 @@ def http_request( if "Retry-After" in result.headers: wait_time = int(result.headers["Retry-After"]) cur_retries += 1 - time.sleep(wait_time) + await asyncio.sleep(wait_time) continue error_message = result.content @@ -572,7 +551,9 @@ def http_request( response_body=result.content, ) - def http_get(self, path, query_data=None, streamed=False, raw=False, **kwargs): + async def http_get( + self, path, query_data=None, streamed=False, raw=False, **kwargs + ): """Make a GET request to the Gitlab server. Args: @@ -593,7 +574,7 @@ def http_get(self, path, query_data=None, streamed=False, raw=False, **kwargs): GitlabParsingError: If the json data could not be parsed """ query_data = query_data or {} - result = self.http_request( + result = await self.http_request( "get", path, query_data=query_data, streamed=streamed, **kwargs ) @@ -611,7 +592,7 @@ def http_get(self, path, query_data=None, streamed=False, raw=False, **kwargs): else: return result - def http_list(self, path, query_data=None, as_list=None, **kwargs): + async def http_list(self, path, query_data=None, as_list=None, **kwargs): """Make a GET request to the Gitlab server for list-oriented queries. Args: @@ -641,16 +622,20 @@ def http_list(self, path, query_data=None, as_list=None, **kwargs): url = self._build_url(path) if get_all is True and as_list is True: - return list(GitlabList(self, url, query_data, **kwargs)) + return list(await GitlabList.create(self, url, query_data, **kwargs)) if "page" in kwargs or as_list is True: # pagination requested, we return a list - return list(GitlabList(self, url, query_data, get_next=False, **kwargs)) + return list( + await GitlabList.create(self, url, query_data, get_next=False, **kwargs) + ) # No pagination, generator requested - return GitlabList(self, url, query_data, **kwargs) + return await GitlabList.create(self, url, query_data, **kwargs) - def http_post(self, path, query_data=None, post_data=None, files=None, **kwargs): + async def http_post( + self, path, query_data=None, post_data=None, files=None, **kwargs + ): """Make a POST request to the Gitlab server. Args: @@ -673,7 +658,7 @@ def http_post(self, path, query_data=None, post_data=None, files=None, **kwargs) query_data = query_data or {} post_data = post_data or {} - result = self.http_request( + result = await self.http_request( "post", path, query_data=query_data, @@ -688,7 +673,9 @@ def http_post(self, path, query_data=None, post_data=None, files=None, **kwargs) raise GitlabParsingError(error_message="Failed to parse the server message") return result - def http_put(self, path, query_data=None, post_data=None, files=None, **kwargs): + async def http_put( + self, path, query_data=None, post_data=None, files=None, **kwargs + ): """Make a PUT request to the Gitlab server. Args: @@ -710,7 +697,7 @@ def http_put(self, path, query_data=None, post_data=None, files=None, **kwargs): query_data = query_data or {} post_data = post_data or {} - result = self.http_request( + result = await self.http_request( "put", path, query_data=query_data, @@ -723,7 +710,7 @@ def http_put(self, path, query_data=None, post_data=None, files=None, **kwargs): except Exception: raise GitlabParsingError(error_message="Failed to parse the server message") - def http_delete(self, path, **kwargs): + async def http_delete(self, path, **kwargs): """Make a PUT request to the Gitlab server. Args: @@ -737,10 +724,10 @@ def http_delete(self, path, **kwargs): Raises: GitlabHttpError: When the return code is not 2xx """ - return self.http_request("delete", path, **kwargs) + return await self.http_request("delete", path, **kwargs) @on_http_error(GitlabSearchError) - def search(self, scope, search, **kwargs): + async def search(self, scope, search, **kwargs): """Search GitLab resources matching the provided string.' Args: @@ -756,7 +743,7 @@ def search(self, scope, search, **kwargs): GitlabList: A list of dicts describing the resources found. """ data = {"scope": scope, "search": search} - return self.http_list("/search", query_data=data, **kwargs) + return await self.http_list("/search", query_data=data, **kwargs) class GitlabList(object): @@ -766,14 +753,24 @@ class GitlabList(object): the API again when needed. """ - def __init__(self, gl, url, query_data, get_next=True, **kwargs): + @classmethod + async def create(cls, gl, url, query_data, get_next=True, **kwargs): + """Create GitlabList with data + + Create is made in factory way since it's cleaner to use such way + instead of make async __init__ + """ + self = GitlabList() self._gl = gl - self._query(url, query_data, **kwargs) + await self._query(url, query_data, **kwargs) self._get_next = get_next + return self - def _query(self, url, query_data=None, **kwargs): + async def _query(self, url, query_data=None, **kwargs): query_data = query_data or {} - result = self._gl.http_request("get", url, query_data=query_data, **kwargs) + result = await self._gl.http_request( + "get", url, query_data=query_data, **kwargs + ) try: self._next_url = result.links["next"]["url"] except KeyError: @@ -828,16 +825,10 @@ def total(self): """The total number of items.""" return int(self._total) - def __iter__(self): - return self - - def __len__(self): - return int(self._total) + async def __aiter__(self): + return await self - def __next__(self): - return self.next() - - def next(self): + async def __anext__(self): try: item = self._data[self._current] self._current += 1 @@ -846,7 +837,10 @@ def next(self): pass if self._next_url and self._get_next is True: - self._query(self._next_url) - return self.next() + await self._query(self._next_url) + return await self.next() + + raise StopAsyncIteration - raise StopIteration + def __len__(self): + return int(self._total) diff --git a/gitlab/cli.py b/gitlab/cli.py index 8fc30bc36..629974e77 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -39,8 +39,8 @@ def register_custom_action(cls_names, mandatory=tuple(), optional=tuple()): def wrap(f): @functools.wraps(f) - def wrapped_f(*args, **kwargs): - return f(*args, **kwargs) + async def wrapped_f(*args, **kwargs): + return await f(*args, **kwargs) # in_obj defines whether the method belongs to the obj or the manager in_obj = True diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index aff3c87d5..1137be97d 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -262,9 +262,9 @@ def on_http_error(error): def wrap(f): @functools.wraps(f) - def wrapped_f(*args, **kwargs): + async def wrapped_f(*args, **kwargs): try: - return f(*args, **kwargs) + return await f(*args, **kwargs) except GitlabHttpError as e: raise error(e.error_message, e.response_code, e.response_body) diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 854449949..271119396 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -16,8 +16,7 @@ # along with this program. If not, see . import gitlab -from gitlab import base -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import types as g_types from gitlab import utils @@ -25,7 +24,7 @@ class GetMixin(object): @exc.on_http_error(exc.GitlabGetError) - def get(self, id, lazy=False, **kwargs): + async def get(self, id, lazy=False, **kwargs): """Retrieve a single object. Args: @@ -48,12 +47,12 @@ def get(self, id, lazy=False, **kwargs): if lazy is True: return self._obj_cls(self, {self._obj_cls._id_attr: id}) server_data = self.gitlab.http_get(path, **kwargs) - return self._obj_cls(self, server_data) + return await self._obj_cls(self, server_data) class GetWithoutIdMixin(object): @exc.on_http_error(exc.GitlabGetError) - def get(self, id=None, **kwargs): + async def get(self, id=None, **kwargs): """Retrieve a single object. Args: @@ -69,12 +68,12 @@ def get(self, id=None, **kwargs): server_data = self.gitlab.http_get(self.path, **kwargs) if server_data is None: return None - return self._obj_cls(self, server_data) + return await self._obj_cls(self, server_data) class RefreshMixin(object): @exc.on_http_error(exc.GitlabGetError) - def refresh(self, **kwargs): + async def refresh(self, **kwargs): """Refresh a single object from server. Args: @@ -90,13 +89,13 @@ def refresh(self, **kwargs): path = "%s/%s" % (self.manager.path, self.id) else: path = self.manager.path - server_data = self.manager.gitlab.http_get(path, **kwargs) + server_data = await self.manager.gitlab.http_get(path, **kwargs) self._update_attrs(server_data) class ListMixin(object): @exc.on_http_error(exc.GitlabListError) - def list(self, **kwargs): + async def list(self, **kwargs): """Retrieve a list of objects. Args: @@ -138,7 +137,7 @@ def list(self, **kwargs): # Allow to overwrite the path, handy for custom listings path = data.pop("path", self.path) - obj = self.gitlab.http_list(path, **data) + obj = await self.gitlab.http_list(path, **data) if isinstance(obj, list): return [self._obj_cls(self, item) for item in obj] else: @@ -170,7 +169,7 @@ def get_create_attrs(self): return getattr(self, "_create_attrs", (tuple(), tuple())) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Create a new object. Args: @@ -208,7 +207,9 @@ def create(self, data, **kwargs): # Handle specific URL for creation path = kwargs.pop("path", self.path) - server_data = self.gitlab.http_post(path, post_data=data, files=files, **kwargs) + server_data = await self.gitlab.http_post( + path, post_data=data, files=files, **kwargs + ) return self._obj_cls(self, server_data) @@ -247,7 +248,7 @@ def _get_update_method(self): return http_method @exc.on_http_error(exc.GitlabUpdateError) - def update(self, id=None, new_data=None, **kwargs): + async def update(self, id=None, new_data=None, **kwargs): """Update an object on the server. Args: @@ -290,12 +291,12 @@ def update(self, id=None, new_data=None, **kwargs): new_data[attr_name] = type_obj.get_for_api() http_method = self._get_update_method() - return http_method(path, post_data=new_data, files=files, **kwargs) + return await http_method(path, post_data=new_data, files=files, **kwargs) class SetMixin(object): @exc.on_http_error(exc.GitlabSetError) - def set(self, key, value, **kwargs): + async def set(self, key, value, **kwargs): """Create or update the object. Args: @@ -312,13 +313,13 @@ def set(self, key, value, **kwargs): """ path = "%s/%s" % (self.path, utils.clean_str_id(key)) data = {"value": value} - server_data = self.gitlab.http_put(path, post_data=data, **kwargs) + server_data = await self.gitlab.http_put(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) class DeleteMixin(object): @exc.on_http_error(exc.GitlabDeleteError) - def delete(self, id, **kwargs): + async def delete(self, id, **kwargs): """Delete an object on the server. Args: @@ -335,7 +336,7 @@ def delete(self, id, **kwargs): if not isinstance(id, int): id = utils.clean_str_id(id) path = "%s/%s" % (self.path, id) - self.gitlab.http_delete(path, **kwargs) + await self.gitlab.http_delete(path, **kwargs) class CRUDMixin(GetMixin, ListMixin, CreateMixin, UpdateMixin, DeleteMixin): @@ -360,7 +361,7 @@ def _get_updated_data(self): return updated_data - def save(self, **kwargs): + async def save(self, **kwargs): """Save the changes made to the object to the server. The object is updated to match what the server returns. @@ -379,7 +380,7 @@ def save(self, **kwargs): # call the manager obj_id = self.get_id() - server_data = self.manager.update(obj_id, updated_data, **kwargs) + server_data = await self.manager.update(obj_id, updated_data, **kwargs) if server_data is not None: self._update_attrs(server_data) @@ -387,7 +388,7 @@ def save(self, **kwargs): class ObjectDeleteMixin(object): """Mixin for RESTObject's that can be deleted.""" - def delete(self, **kwargs): + async def delete(self, **kwargs): """Delete the object from the server. Args: @@ -397,13 +398,13 @@ def delete(self, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ - self.manager.delete(self.get_id()) + await self.manager.delete(self.get_id()) class UserAgentDetailMixin(object): @cli.register_custom_action(("Snippet", "ProjectSnippet", "ProjectIssue")) @exc.on_http_error(exc.GitlabGetError) - def user_agent_detail(self, **kwargs): + async def user_agent_detail(self, **kwargs): """Get the user agent detail. Args: @@ -414,7 +415,7 @@ def user_agent_detail(self, **kwargs): GitlabGetError: If the server cannot perform the request """ path = "%s/%s/user_agent_detail" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) class AccessRequestMixin(object): @@ -422,7 +423,7 @@ class AccessRequestMixin(object): ("ProjectAccessRequest", "GroupAccessRequest"), tuple(), ("access_level",) ) @exc.on_http_error(exc.GitlabUpdateError) - def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): + async def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): """Approve an access request. Args: @@ -436,7 +437,7 @@ def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): path = "%s/%s/approve" % (self.manager.path, self.id) data = {"access_level": access_level} - server_data = self.manager.gitlab.http_put(path, post_data=data, **kwargs) + server_data = await self.manager.gitlab.http_put(path, post_data=data, **kwargs) self._update_attrs(server_data) @@ -445,7 +446,7 @@ class SubscribableMixin(object): ("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel") ) @exc.on_http_error(exc.GitlabSubscribeError) - def subscribe(self, **kwargs): + async def subscribe(self, **kwargs): """Subscribe to the object notifications. Args: @@ -456,14 +457,14 @@ def subscribe(self, **kwargs): GitlabSubscribeError: If the subscription cannot be done """ path = "%s/%s/subscribe" % (self.manager.path, self.get_id()) - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action( ("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel") ) @exc.on_http_error(exc.GitlabUnsubscribeError) - def unsubscribe(self, **kwargs): + async def unsubscribe(self, **kwargs): """Unsubscribe from the object notifications. Args: @@ -474,14 +475,14 @@ def unsubscribe(self, **kwargs): GitlabUnsubscribeError: If the unsubscription cannot be done """ path = "%s/%s/unsubscribe" % (self.manager.path, self.get_id()) - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) class TodoMixin(object): @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTodoError) - def todo(self, **kwargs): + async def todo(self, **kwargs): """Create a todo associated to the object. Args: @@ -492,13 +493,13 @@ def todo(self, **kwargs): GitlabTodoError: If the todo cannot be set """ path = "%s/%s/todo" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path, **kwargs) + await self.manager.gitlab.http_post(path, **kwargs) class TimeTrackingMixin(object): @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTimeTrackingError) - def time_stats(self, **kwargs): + async def time_stats(self, **kwargs): """Get time stats for the object. Args: @@ -514,11 +515,11 @@ def time_stats(self, **kwargs): return self.attributes["time_stats"] path = "%s/%s/time_stats" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",)) @exc.on_http_error(exc.GitlabTimeTrackingError) - def time_estimate(self, duration, **kwargs): + async def time_estimate(self, duration, **kwargs): """Set an estimated time of work for the object. Args: @@ -531,11 +532,11 @@ def time_estimate(self, duration, **kwargs): """ path = "%s/%s/time_estimate" % (self.manager.path, self.get_id()) data = {"duration": duration} - return self.manager.gitlab.http_post(path, post_data=data, **kwargs) + return await self.manager.gitlab.http_post(path, post_data=data, **kwargs) @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTimeTrackingError) - def reset_time_estimate(self, **kwargs): + async def reset_time_estimate(self, **kwargs): """Resets estimated time for the object to 0 seconds. Args: @@ -546,11 +547,11 @@ def reset_time_estimate(self, **kwargs): GitlabTimeTrackingError: If the time tracking update cannot be done """ path = "%s/%s/reset_time_estimate" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_post(path, **kwargs) + return await self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",)) @exc.on_http_error(exc.GitlabTimeTrackingError) - def add_spent_time(self, duration, **kwargs): + async def add_spent_time(self, duration, **kwargs): """Add time spent working on the object. Args: @@ -563,11 +564,11 @@ def add_spent_time(self, duration, **kwargs): """ path = "%s/%s/add_spent_time" % (self.manager.path, self.get_id()) data = {"duration": duration} - return self.manager.gitlab.http_post(path, post_data=data, **kwargs) + return await self.manager.gitlab.http_post(path, post_data=data, **kwargs) @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTimeTrackingError) - def reset_spent_time(self, **kwargs): + async def reset_spent_time(self, **kwargs): """Resets the time spent working on the object. Args: @@ -578,13 +579,13 @@ def reset_spent_time(self, **kwargs): GitlabTimeTrackingError: If the time tracking update cannot be done """ path = "%s/%s/reset_spent_time" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_post(path, **kwargs) + return await self.manager.gitlab.http_post(path, **kwargs) class ParticipantsMixin(object): @cli.register_custom_action(("ProjectMergeRequest", "ProjectIssue")) @exc.on_http_error(exc.GitlabListError) - def participants(self, **kwargs): + async def participants(self, **kwargs): """List the participants. Args: @@ -604,7 +605,7 @@ def participants(self, **kwargs): """ path = "%s/%s/participants" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) class BadgeRenderMixin(object): @@ -612,7 +613,7 @@ class BadgeRenderMixin(object): ("GroupBadgeManager", "ProjectBadgeManager"), ("link_url", "image_url") ) @exc.on_http_error(exc.GitlabRenderError) - def render(self, link_url, image_url, **kwargs): + async def render(self, link_url, image_url, **kwargs): """Preview link_url and image_url after interpolation. Args: @@ -629,4 +630,4 @@ def render(self, link_url, image_url, **kwargs): """ path = "%s/render" % self.path data = {"link_url": link_url, "image_url": image_url} - return self.gitlab.http_get(path, data, **kwargs) + return await self.gitlab.http_get(path, data, **kwargs) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index b31870c2b..ad6101d5b 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -15,16 +15,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from __future__ import print_function -from __future__ import absolute_import +from __future__ import absolute_import, print_function + import base64 +from gitlab import cli, types, utils from gitlab.base import * # noqa -from gitlab import cli from gitlab.exceptions import * # noqa from gitlab.mixins import * # noqa -from gitlab import types -from gitlab import utils VISIBILITY_PRIVATE = "private" VISIBILITY_INTERNAL = "internal" @@ -46,7 +44,7 @@ class SidekiqManager(RESTManager): @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def queue_metrics(self, **kwargs): + async def queue_metrics(self, **kwargs): """Return the registred queues information. Args: @@ -59,11 +57,11 @@ def queue_metrics(self, **kwargs): Returns: dict: Information about the Sidekiq queues """ - return self.gitlab.http_get("/sidekiq/queue_metrics", **kwargs) + return await self.gitlab.http_get("/sidekiq/queue_metrics", **kwargs) @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def process_metrics(self, **kwargs): + async def process_metrics(self, **kwargs): """Return the registred sidekiq workers. Args: @@ -76,11 +74,11 @@ def process_metrics(self, **kwargs): Returns: dict: Information about the register Sidekiq worker """ - return self.gitlab.http_get("/sidekiq/process_metrics", **kwargs) + return await self.gitlab.http_get("/sidekiq/process_metrics", **kwargs) @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def job_stats(self, **kwargs): + async def job_stats(self, **kwargs): """Return statistics about the jobs performed. Args: @@ -93,11 +91,11 @@ def job_stats(self, **kwargs): Returns: dict: Statistics about the Sidekiq jobs performed """ - return self.gitlab.http_get("/sidekiq/job_stats", **kwargs) + return await self.gitlab.http_get("/sidekiq/job_stats", **kwargs) @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def compound_metrics(self, **kwargs): + async def compound_metrics(self, **kwargs): """Return all available metrics and statistics. Args: @@ -110,7 +108,7 @@ def compound_metrics(self, **kwargs): Returns: dict: All available Sidekiq metrics and statistics """ - return self.gitlab.http_get("/sidekiq/compound_metrics", **kwargs) + return await self.gitlab.http_get("/sidekiq/compound_metrics", **kwargs) class Event(RESTObject): @@ -277,7 +275,7 @@ class UserProjectManager(ListMixin, CreateMixin, RESTManager): "id_before", ) - def list(self, **kwargs): + async def list(self, **kwargs): """Retrieve a list of objects. Args: @@ -299,7 +297,7 @@ def list(self, **kwargs): path = "/users/%s/projects" % self._parent.id else: path = "/users/%s/projects" % kwargs["user_id"] - return ListMixin.list(self, path=path, **kwargs) + return await ListMixin.list(self, path=path, **kwargs) class User(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -317,7 +315,7 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("User") @exc.on_http_error(exc.GitlabBlockError) - def block(self, **kwargs): + async def block(self, **kwargs): """Block the user. Args: @@ -331,14 +329,14 @@ def block(self, **kwargs): bool: Whether the user status has been changed """ path = "/users/%s/block" % self.id - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) if server_data is True: self._attrs["state"] = "blocked" return server_data @cli.register_custom_action("User") @exc.on_http_error(exc.GitlabUnblockError) - def unblock(self, **kwargs): + async def unblock(self, **kwargs): """Unblock the user. Args: @@ -352,14 +350,14 @@ def unblock(self, **kwargs): bool: Whether the user status has been changed """ path = "/users/%s/unblock" % self.id - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) if server_data is True: self._attrs["state"] = "active" return server_data @cli.register_custom_action("User") @exc.on_http_error(exc.GitlabDeactivateError) - def deactivate(self, **kwargs): + async def deactivate(self, **kwargs): """Deactivate the user. Args: @@ -373,14 +371,14 @@ def deactivate(self, **kwargs): bool: Whether the user status has been changed """ path = "/users/%s/deactivate" % self.id - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) if server_data: self._attrs["state"] = "deactivated" return server_data @cli.register_custom_action("User") @exc.on_http_error(exc.GitlabActivateError) - def activate(self, **kwargs): + async def activate(self, **kwargs): """Activate the user. Args: @@ -394,7 +392,7 @@ def activate(self, **kwargs): bool: Whether the user status has been changed """ path = "/users/%s/activate" % self.id - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) if server_data: self._attrs["state"] = "active" return server_data @@ -551,7 +549,7 @@ class ApplicationAppearanceManager(GetWithoutIdMixin, UpdateMixin, RESTManager): ) @exc.on_http_error(exc.GitlabUpdateError) - def update(self, id=None, new_data=None, **kwargs): + async def update(self, id=None, new_data=None, **kwargs): """Update an object on the server. Args: @@ -568,7 +566,7 @@ def update(self, id=None, new_data=None, **kwargs): """ new_data = new_data or {} data = new_data.copy() - super(ApplicationAppearanceManager, self).update(id, data, **kwargs) + await super(ApplicationAppearanceManager, self).update(id, data, **kwargs) class ApplicationSettings(SaveMixin, RESTObject): @@ -636,7 +634,7 @@ class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): ) @exc.on_http_error(exc.GitlabUpdateError) - def update(self, id=None, new_data=None, **kwargs): + async def update(self, id=None, new_data=None, **kwargs): """Update an object on the server. Args: @@ -655,7 +653,7 @@ def update(self, id=None, new_data=None, **kwargs): data = new_data.copy() if "domain_whitelist" in data and data["domain_whitelist"] is None: data.pop("domain_whitelist") - super(ApplicationSettingsManager, self).update(id, data, **kwargs) + await super(ApplicationSettingsManager, self).update(id, data, **kwargs) class BroadcastMessage(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -724,7 +722,7 @@ class FeatureManager(ListMixin, DeleteMixin, RESTManager): _obj_cls = Feature @exc.on_http_error(exc.GitlabSetError) - def set( + async def set( self, name, value, @@ -761,7 +759,7 @@ def set( "project": project, } data = utils.remove_none_from_dict(data) - server_data = self.gitlab.http_post(path, post_data=data, **kwargs) + server_data = await self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) @@ -852,7 +850,7 @@ class GroupClusterManager(CRUDMixin, RESTManager): ) @exc.on_http_error(exc.GitlabStopError) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Create a new object. Args: @@ -870,7 +868,7 @@ def create(self, data, **kwargs): the data sent by the server """ path = "%s/user" % (self.path) - return CreateMixin.create(self, data, path=path, **kwargs) + return await CreateMixin.create(self, data, path=path, **kwargs) class GroupCustomAttribute(ObjectDeleteMixin, RESTObject): @@ -886,7 +884,7 @@ class GroupCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin, RESTMana class GroupEpicIssue(ObjectDeleteMixin, SaveMixin, RESTObject): _id_attr = "epic_issue_id" - def save(self, **kwargs): + async def save(self, **kwargs): """Save the changes made to the object to the server. The object is updated to match what the server returns. @@ -905,7 +903,7 @@ def save(self, **kwargs): # call the manager obj_id = self.get_id() - self.manager.update(obj_id, updated_data, **kwargs) + await self.manager.update(obj_id, updated_data, **kwargs) class GroupEpicIssueManager( @@ -918,7 +916,7 @@ class GroupEpicIssueManager( _update_attrs = (tuple(), ("move_before_id", "move_after_id")) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Create a new object. Args: @@ -936,7 +934,7 @@ def create(self, data, **kwargs): """ CreateMixin._check_missing_create_attrs(self, data) path = "%s/%s" % (self.path, data.pop("issue_id")) - server_data = self.gitlab.http_post(path, **kwargs) + server_data = await self.gitlab.http_post(path, **kwargs) # The epic_issue_id attribute doesn't exist when creating the resource, # but is used everywhere elese. Let's create it to be consistent client # side @@ -1007,7 +1005,7 @@ class GroupLabel(SubscribableMixin, SaveMixin, ObjectDeleteMixin, RESTObject): # Update without ID, but we need an ID to get from list. @exc.on_http_error(exc.GitlabUpdateError) - def save(self, **kwargs): + async def save(self, **kwargs): """Saves the changes made to the object to the server. The object is updated to match what the server returns. @@ -1022,7 +1020,7 @@ def save(self, **kwargs): updated_data = self._get_updated_data() # call the manager - server_data = self.manager.update(None, updated_data, **kwargs) + server_data = await self.manager.update(None, updated_data, **kwargs) self._update_attrs(server_data) @@ -1034,7 +1032,7 @@ class GroupLabelManager(ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa _update_attrs = (("name",), ("new_name", "color", "description", "priority")) # Update without ID. - def update(self, name, new_data=None, **kwargs): + async def update(self, name, new_data=None, **kwargs): """Update a Label on the server. Args: @@ -1044,11 +1042,11 @@ def update(self, name, new_data=None, **kwargs): new_data = new_data or {} if name: new_data["name"] = name - return super().update(id=None, new_data=new_data, **kwargs) + return await super().update(id=None, new_data=new_data, **kwargs) # Delete without ID. @exc.on_http_error(exc.GitlabDeleteError) - def delete(self, name, **kwargs): + async def delete(self, name, **kwargs): """Delete a Label on the server. Args: @@ -1059,7 +1057,7 @@ def delete(self, name, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ - self.gitlab.http_delete(self.path, query_data={"name": name}, **kwargs) + await self.gitlab.http_delete(self.path, query_data={"name": name}, **kwargs) class GroupMember(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -1075,7 +1073,7 @@ class GroupMemberManager(CRUDMixin, RESTManager): @cli.register_custom_action("GroupMemberManager") @exc.on_http_error(exc.GitlabListError) - def all(self, **kwargs): + async def all(self, **kwargs): """List all the members, included inherited ones. Args: @@ -1095,7 +1093,7 @@ def all(self, **kwargs): """ path = "%s/all" % self.path - obj = self.gitlab.http_list(path, **kwargs) + obj = await self.gitlab.http_list(path, **kwargs) return [self._obj_cls(self, item) for item in obj] @@ -1134,7 +1132,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("GroupMilestone") @exc.on_http_error(exc.GitlabListError) - def issues(self, **kwargs): + async def issues(self, **kwargs): """List issues related to this milestone. Args: @@ -1154,14 +1152,14 @@ def issues(self, **kwargs): """ path = "%s/%s/issues" % (self.manager.path, self.get_id()) - data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + data_list = await self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, GroupIssue, data_list) @cli.register_custom_action("GroupMilestone") @exc.on_http_error(exc.GitlabListError) - def merge_requests(self, **kwargs): + async def merge_requests(self, **kwargs): """List the merge requests related to this milestone. Args: @@ -1180,7 +1178,7 @@ def merge_requests(self, **kwargs): RESTObjectList: The list of merge requests """ path = "%s/%s/merge_requests" % (self.manager.path, self.get_id()) - data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + data_list = await self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, GroupMergeRequest, data_list) @@ -1289,7 +1287,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("Group", ("to_project_id",)) @exc.on_http_error(exc.GitlabTransferProjectError) - def transfer_project(self, to_project_id, **kwargs): + async def transfer_project(self, to_project_id, **kwargs): """Transfer a project to this group. Args: @@ -1301,11 +1299,11 @@ def transfer_project(self, to_project_id, **kwargs): GitlabTransferProjectError: If the project could not be transfered """ path = "/groups/%s/projects/%s" % (self.id, to_project_id) - self.manager.gitlab.http_post(path, **kwargs) + await self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action("Group", ("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) - def search(self, scope, search, **kwargs): + async def search(self, scope, search, **kwargs): """Search the group resources matching the provided string.' Args: @@ -1322,11 +1320,11 @@ def search(self, scope, search, **kwargs): """ data = {"scope": scope, "search": search} path = "/groups/%s/search" % self.get_id() - return self.manager.gitlab.http_list(path, query_data=data, **kwargs) + return await self.manager.gitlab.http_list(path, query_data=data, **kwargs) @cli.register_custom_action("Group", ("cn", "group_access", "provider")) @exc.on_http_error(exc.GitlabCreateError) - def add_ldap_group_link(self, cn, group_access, provider, **kwargs): + async def add_ldap_group_link(self, cn, group_access, provider, **kwargs): """Add an LDAP group link. Args: @@ -1342,11 +1340,11 @@ def add_ldap_group_link(self, cn, group_access, provider, **kwargs): """ path = "/groups/%s/ldap_group_links" % self.get_id() data = {"cn": cn, "group_access": group_access, "provider": provider} - self.manager.gitlab.http_post(path, post_data=data, **kwargs) + await self.manager.gitlab.http_post(path, post_data=data, **kwargs) @cli.register_custom_action("Group", ("cn",), ("provider",)) @exc.on_http_error(exc.GitlabDeleteError) - def delete_ldap_group_link(self, cn, provider=None, **kwargs): + async def delete_ldap_group_link(self, cn, provider=None, **kwargs): """Delete an LDAP group link. Args: @@ -1362,11 +1360,11 @@ def delete_ldap_group_link(self, cn, provider=None, **kwargs): if provider is not None: path += "/%s" % provider path += "/%s" % cn - self.manager.gitlab.http_delete(path) + await self.manager.gitlab.http_delete(path) @cli.register_custom_action("Group") @exc.on_http_error(exc.GitlabCreateError) - def ldap_sync(self, **kwargs): + async def ldap_sync(self, **kwargs): """Sync LDAP groups. Args: @@ -1377,7 +1375,7 @@ def ldap_sync(self, **kwargs): GitlabCreateError: If the server cannot perform the request """ path = "/groups/%s/ldap_sync" % self.get_id() - self.manager.gitlab.http_post(path, **kwargs) + await self.manager.gitlab.http_post(path, **kwargs) class GroupManager(CRUDMixin, RESTManager): @@ -1465,7 +1463,7 @@ class LDAPGroupManager(RESTManager): _list_filters = ("search", "provider") @exc.on_http_error(exc.GitlabListError) - def list(self, **kwargs): + async def list(self, **kwargs): """Retrieve a list of objects. Args: @@ -1492,7 +1490,7 @@ def list(self, **kwargs): else: path = self._path - obj = self.gitlab.http_list(path, **data) + obj = await self.gitlab.http_list(path, **data) if isinstance(obj, list): return [self._obj_cls(self, item) for item in obj] else: @@ -1545,7 +1543,7 @@ class Snippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("Snippet") @exc.on_http_error(exc.GitlabGetError) - def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): + async def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the content of a snippet. Args: @@ -1565,7 +1563,7 @@ def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): str: The snippet content """ path = "/snippets/%s/raw" % self.get_id() - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @@ -1578,7 +1576,7 @@ class SnippetManager(CRUDMixin, RESTManager): _update_attrs = (tuple(), ("title", "file_name", "content", "visibility")) @cli.register_custom_action("SnippetManager") - def public(self, **kwargs): + async def public(self, **kwargs): """List all the public snippets. Args: @@ -1591,7 +1589,7 @@ def public(self, **kwargs): Returns: RESTObjectList: A generator for the snippets list """ - return self.list(path="/snippets/public", **kwargs) + return await self.list(path="/snippets/public", **kwargs) class Namespace(RESTObject): @@ -1636,7 +1634,7 @@ class ProjectRegistryTagManager(DeleteMixin, RetrieveMixin, RESTManager): "ProjectRegistryTagManager", optional=("name_regex", "keep_n", "older_than") ) @exc.on_http_error(exc.GitlabDeleteError) - def delete_in_bulk(self, name_regex=".*", **kwargs): + async def delete_in_bulk(self, name_regex=".*", **kwargs): """Delete Tag in bulk Args: @@ -1653,7 +1651,7 @@ def delete_in_bulk(self, name_regex=".*", **kwargs): valid_attrs = ["keep_n", "older_than"] data = {"name_regex": name_regex} data.update({k: v for k, v in kwargs.items() if k in valid_attrs}) - self.gitlab.http_delete(self.path, query_data=data, **kwargs) + await self.gitlab.http_delete(self.path, query_data=data, **kwargs) class ProjectBoardList(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -1686,7 +1684,9 @@ class ProjectBranch(ObjectDeleteMixin, RESTObject): "ProjectBranch", tuple(), ("developers_can_push", "developers_can_merge") ) @exc.on_http_error(exc.GitlabProtectError) - def protect(self, developers_can_push=False, developers_can_merge=False, **kwargs): + async def protect( + self, developers_can_push=False, developers_can_merge=False, **kwargs + ): """Protect the branch. Args: @@ -1706,12 +1706,12 @@ def protect(self, developers_can_push=False, developers_can_merge=False, **kwarg "developers_can_push": developers_can_push, "developers_can_merge": developers_can_merge, } - self.manager.gitlab.http_put(path, post_data=post_data, **kwargs) + await self.manager.gitlab.http_put(path, post_data=post_data, **kwargs) self._attrs["protected"] = True @cli.register_custom_action("ProjectBranch") @exc.on_http_error(exc.GitlabProtectError) - def unprotect(self, **kwargs): + async def unprotect(self, **kwargs): """Unprotect the branch. Args: @@ -1723,7 +1723,7 @@ def unprotect(self, **kwargs): """ id = self.get_id().replace("/", "%2F") path = "%s/%s/unprotect" % (self.manager.path, id) - self.manager.gitlab.http_put(path, **kwargs) + await self.manager.gitlab.http_put(path, **kwargs) self._attrs["protected"] = False @@ -1758,7 +1758,7 @@ class ProjectClusterManager(CRUDMixin, RESTManager): ) @exc.on_http_error(exc.GitlabStopError) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Create a new object. Args: @@ -1776,7 +1776,7 @@ def create(self, data, **kwargs): the data sent by the server """ path = "%s/user" % (self.path) - return CreateMixin.create(self, data, path=path, **kwargs) + return await CreateMixin.create(self, data, path=path, **kwargs) class ProjectCustomAttribute(ObjectDeleteMixin, RESTObject): @@ -1792,7 +1792,7 @@ class ProjectCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin, RESTMa class ProjectJob(RESTObject, RefreshMixin): @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobCancelError) - def cancel(self, **kwargs): + async def cancel(self, **kwargs): """Cancel the job. Args: @@ -1803,11 +1803,11 @@ def cancel(self, **kwargs): GitlabJobCancelError: If the job could not be canceled """ path = "%s/%s/cancel" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path) + await self.manager.gitlab.http_post(path) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobRetryError) - def retry(self, **kwargs): + async def retry(self, **kwargs): """Retry the job. Args: @@ -1818,11 +1818,11 @@ def retry(self, **kwargs): GitlabJobRetryError: If the job could not be retried """ path = "%s/%s/retry" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path) + await self.manager.gitlab.http_post(path) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobPlayError) - def play(self, **kwargs): + async def play(self, **kwargs): """Trigger a job explicitly. Args: @@ -1833,11 +1833,11 @@ def play(self, **kwargs): GitlabJobPlayError: If the job could not be triggered """ path = "%s/%s/play" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path) + await self.manager.gitlab.http_post(path) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobEraseError) - def erase(self, **kwargs): + async def erase(self, **kwargs): """Erase the job (remove job artifacts and trace). Args: @@ -1848,11 +1848,11 @@ def erase(self, **kwargs): GitlabJobEraseError: If the job could not be erased """ path = "%s/%s/erase" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path) + await self.manager.gitlab.http_post(path) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabCreateError) - def keep_artifacts(self, **kwargs): + async def keep_artifacts(self, **kwargs): """Prevent artifacts from being deleted when expiration is set. Args: @@ -1863,11 +1863,11 @@ def keep_artifacts(self, **kwargs): GitlabCreateError: If the request could not be performed """ path = "%s/%s/artifacts/keep" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path) + await self.manager.gitlab.http_post(path) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabCreateError) - def delete_artifacts(self, **kwargs): + async def delete_artifacts(self, **kwargs): """Delete artifacts of a job. Args: @@ -1878,11 +1878,11 @@ def delete_artifacts(self, **kwargs): GitlabDeleteError: If the request could not be performed """ path = "%s/%s/artifacts" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_delete(path) + await self.manager.gitlab.http_delete(path) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) - def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): + async def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Get the job artifacts. Args: @@ -1902,14 +1902,16 @@ def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): str: The artifacts if `streamed` is False, None otherwise. """ path = "%s/%s/artifacts" % (self.manager.path, self.get_id()) - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) - def artifact(self, path, streamed=False, action=None, chunk_size=1024, **kwargs): + async def artifact( + self, path, streamed=False, action=None, chunk_size=1024, **kwargs + ): """Get a single artifact file from within the job's artifacts archive. Args: @@ -1930,14 +1932,14 @@ def artifact(self, path, streamed=False, action=None, chunk_size=1024, **kwargs) str: The artifacts if `streamed` is False, None otherwise. """ path = "%s/%s/artifacts/%s" % (self.manager.path, self.get_id(), path) - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) - def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): + async def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Get the job trace. Args: @@ -1957,7 +1959,7 @@ def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): str: The trace """ path = "%s/%s/trace" % (self.manager.path, self.get_id()) - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @@ -1983,7 +1985,7 @@ class ProjectCommitStatusManager(ListMixin, CreateMixin, RESTManager): ) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Create a new object. Args: @@ -2065,7 +2067,7 @@ class ProjectCommit(RESTObject): @cli.register_custom_action("ProjectCommit") @exc.on_http_error(exc.GitlabGetError) - def diff(self, **kwargs): + async def diff(self, **kwargs): """Generate the commit diff. Args: @@ -2079,11 +2081,11 @@ def diff(self, **kwargs): list: The changes done in this commit """ path = "%s/%s/diff" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("ProjectCommit", ("branch",)) @exc.on_http_error(exc.GitlabCherryPickError) - def cherry_pick(self, branch, **kwargs): + async def cherry_pick(self, branch, **kwargs): """Cherry-pick a commit into a branch. Args: @@ -2096,11 +2098,11 @@ def cherry_pick(self, branch, **kwargs): """ path = "%s/%s/cherry_pick" % (self.manager.path, self.get_id()) post_data = {"branch": branch} - self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + await self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) @cli.register_custom_action("ProjectCommit", optional=("type",)) @exc.on_http_error(exc.GitlabGetError) - def refs(self, type="all", **kwargs): + async def refs(self, type="all", **kwargs): """List the references the commit is pushed to. Args: @@ -2116,11 +2118,11 @@ def refs(self, type="all", **kwargs): """ path = "%s/%s/refs" % (self.manager.path, self.get_id()) data = {"type": type} - return self.manager.gitlab.http_get(path, query_data=data, **kwargs) + return await self.manager.gitlab.http_get(path, query_data=data, **kwargs) @cli.register_custom_action("ProjectCommit") @exc.on_http_error(exc.GitlabGetError) - def merge_requests(self, **kwargs): + async def merge_requests(self, **kwargs): """List the merge requests related to the commit. Args: @@ -2134,7 +2136,7 @@ def merge_requests(self, **kwargs): list: The merge requests related to the commit. """ path = "%s/%s/merge_requests" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager): @@ -2150,7 +2152,7 @@ class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager): class ProjectEnvironment(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("ProjectEnvironment") @exc.on_http_error(exc.GitlabStopError) - def stop(self, **kwargs): + async def stop(self, **kwargs): """Stop the environment. Args: @@ -2161,7 +2163,7 @@ def stop(self, **kwargs): GitlabStopError: If the operation failed """ path = "%s/%s/stop" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path, **kwargs) + await self.manager.gitlab.http_post(path, **kwargs) class ProjectEnvironmentManager( @@ -2187,7 +2189,7 @@ class ProjectKeyManager(CRUDMixin, RESTManager): @cli.register_custom_action("ProjectKeyManager", ("key_id",)) @exc.on_http_error(exc.GitlabProjectDeployKeyError) - def enable(self, key_id, **kwargs): + async def enable(self, key_id, **kwargs): """Enable a deploy key for a project. Args: @@ -2199,7 +2201,7 @@ def enable(self, key_id, **kwargs): GitlabProjectDeployKeyError: If the key could not be enabled """ path = "%s/%s/enable" % (self.path, key_id) - self.gitlab.http_post(path, **kwargs) + await self.gitlab.http_post(path, **kwargs) class ProjectBadge(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -2249,7 +2251,7 @@ class ProjectForkManager(CreateMixin, ListMixin, RESTManager): ) _create_attrs = (tuple(), ("namespace",)) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Creates a new object. Args: @@ -2394,7 +2396,7 @@ class ProjectIssueLinkManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): _create_attrs = (("target_project_id", "target_issue_iid"), tuple()) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Create a new object. Args: @@ -2410,7 +2412,7 @@ def create(self, data, **kwargs): GitlabCreateError: If the server cannot perform the request """ self._check_missing_create_attrs(data) - server_data = self.gitlab.http_post(self.path, post_data=data, **kwargs) + server_data = await self.gitlab.http_post(self.path, post_data=data, **kwargs) source_issue = ProjectIssue(self._parent.manager, server_data["source_issue"]) target_issue = ProjectIssue(self._parent.manager, server_data["target_issue"]) return source_issue, target_issue @@ -2448,7 +2450,7 @@ class ProjectIssue( @cli.register_custom_action("ProjectIssue", ("to_project_id",)) @exc.on_http_error(exc.GitlabUpdateError) - def move(self, to_project_id, **kwargs): + async def move(self, to_project_id, **kwargs): """Move the issue to another project. Args: @@ -2461,12 +2463,14 @@ def move(self, to_project_id, **kwargs): """ path = "%s/%s/move" % (self.manager.path, self.get_id()) data = {"to_project_id": to_project_id} - server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) + server_data = await self.manager.gitlab.http_post( + path, post_data=data, **kwargs + ) self._update_attrs(server_data) @cli.register_custom_action("ProjectIssue") @exc.on_http_error(exc.GitlabGetError) - def related_merge_requests(self, **kwargs): + async def related_merge_requests(self, **kwargs): """List merge requests related to the issue. Args: @@ -2480,11 +2484,11 @@ def related_merge_requests(self, **kwargs): list: The list of merge requests. """ path = "%s/%s/related_merge_requests" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("ProjectIssue") @exc.on_http_error(exc.GitlabGetError) - def closed_by(self, **kwargs): + async def closed_by(self, **kwargs): """List merge requests that will close the issue when merged. Args: @@ -2498,7 +2502,7 @@ def closed_by(self, **kwargs): list: The list of merge requests. """ path = "%s/%s/closed_by" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) class ProjectIssueManager(CRUDMixin, RESTManager): @@ -2569,7 +2573,7 @@ class ProjectMemberManager(CRUDMixin, RESTManager): @cli.register_custom_action("ProjectMemberManager") @exc.on_http_error(exc.GitlabListError) - def all(self, **kwargs): + async def all(self, **kwargs): """List all the members, included inherited ones. Args: @@ -2642,7 +2646,7 @@ class ProjectTag(ObjectDeleteMixin, RESTObject): _short_print_attr = "name" @cli.register_custom_action("ProjectTag", ("description",)) - def set_release_description(self, description, **kwargs): + async def set_release_description(self, description, **kwargs): """Set the release notes on the tag. If the release doesn't exist yet, it will be created. If it already @@ -2662,14 +2666,14 @@ def set_release_description(self, description, **kwargs): data = {"description": description} if self.release is None: try: - server_data = self.manager.gitlab.http_post( + server_data = await self.manager.gitlab.http_post( path, post_data=data, **kwargs ) except exc.GitlabHttpError as e: raise exc.GitlabCreateError(e.response_code, e.error_message) else: try: - server_data = self.manager.gitlab.http_put( + server_data = await self.manager.gitlab.http_put( path, post_data=data, **kwargs ) except exc.GitlabHttpError as e: @@ -2708,7 +2712,7 @@ class ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTMan _update_uses_post = True @exc.on_http_error(exc.GitlabUpdateError) - def set_approvers( + async def set_approvers( self, approvals_required, approver_ids=None, approver_group_ids=None, **kwargs ): """Change MR-level allowed approvers and approver groups. @@ -2858,7 +2862,7 @@ class ProjectMergeRequest( @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabMROnBuildSuccessError) - def cancel_merge_when_pipeline_succeeds(self, **kwargs): + async def cancel_merge_when_pipeline_succeeds(self, **kwargs): """Cancel merge when the pipeline succeeds. Args: @@ -2874,12 +2878,12 @@ def cancel_merge_when_pipeline_succeeds(self, **kwargs): self.manager.path, self.get_id(), ) - server_data = self.manager.gitlab.http_put(path, **kwargs) + server_data = await self.manager.gitlab.http_put(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) - def closes_issues(self, **kwargs): + async def closes_issues(self, **kwargs): """List issues that will close on merge." Args: @@ -2898,13 +2902,13 @@ def closes_issues(self, **kwargs): RESTObjectList: List of issues """ path = "%s/%s/closes_issues" % (self.manager.path, self.get_id()) - data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + data_list = await self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent) return RESTObjectList(manager, ProjectIssue, data_list) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) - def commits(self, **kwargs): + async def commits(self, **kwargs): """List the merge request commits. Args: @@ -2924,13 +2928,13 @@ def commits(self, **kwargs): """ path = "%s/%s/commits" % (self.manager.path, self.get_id()) - data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + data_list = await self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectCommitManager(self.manager.gitlab, parent=self.manager._parent) return RESTObjectList(manager, ProjectCommit, data_list) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) - def changes(self, **kwargs): + async def changes(self, **kwargs): """List the merge request changes. Args: @@ -2944,11 +2948,11 @@ def changes(self, **kwargs): RESTObjectList: List of changes """ path = "%s/%s/changes" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) - def pipelines(self, **kwargs): + async def pipelines(self, **kwargs): """List the merge request pipelines. Args: @@ -2963,11 +2967,11 @@ def pipelines(self, **kwargs): """ path = "%s/%s/pipelines" % (self.manager.path, self.get_id()) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("ProjectMergeRequest", tuple(), ("sha")) @exc.on_http_error(exc.GitlabMRApprovalError) - def approve(self, sha=None, **kwargs): + async def approve(self, sha=None, **kwargs): """Approve the merge request. Args: @@ -2983,12 +2987,14 @@ def approve(self, sha=None, **kwargs): if sha: data["sha"] = sha - server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) + server_data = await self.manager.gitlab.http_post( + path, post_data=data, **kwargs + ) self._update_attrs(server_data) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabMRApprovalError) - def unapprove(self, **kwargs): + async def unapprove(self, **kwargs): """Unapprove the merge request. Args: @@ -3001,12 +3007,14 @@ def unapprove(self, **kwargs): path = "%s/%s/unapprove" % (self.manager.path, self.get_id()) data = {} - server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) + server_data = await self.manager.gitlab.http_post( + path, post_data=data, **kwargs + ) self._update_attrs(server_data) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabMRRebaseError) - def rebase(self, **kwargs): + async def rebase(self, **kwargs): """Attempt to rebase the source branch onto the target branch Args: @@ -3018,7 +3026,7 @@ def rebase(self, **kwargs): """ path = "%s/%s/rebase" % (self.manager.path, self.get_id()) data = {} - return self.manager.gitlab.http_put(path, post_data=data, **kwargs) + return await self.manager.gitlab.http_put(path, post_data=data, **kwargs) @cli.register_custom_action( "ProjectMergeRequest", @@ -3030,7 +3038,7 @@ def rebase(self, **kwargs): ), ) @exc.on_http_error(exc.GitlabMRClosedError) - def merge( + async def merge( self, merge_commit_message=None, should_remove_source_branch=False, @@ -3060,7 +3068,7 @@ def merge( if merge_when_pipeline_succeeds: data["merge_when_pipeline_succeeds"] = True - server_data = self.manager.gitlab.http_put(path, post_data=data, **kwargs) + server_data = await self.manager.gitlab.http_put(path, post_data=data, **kwargs) self._update_attrs(server_data) @@ -3124,7 +3132,7 @@ class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("ProjectMilestone") @exc.on_http_error(exc.GitlabListError) - def issues(self, **kwargs): + async def issues(self, **kwargs): """List issues related to this milestone. Args: @@ -3144,14 +3152,14 @@ def issues(self, **kwargs): """ path = "%s/%s/issues" % (self.manager.path, self.get_id()) - data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + data_list = await self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, ProjectIssue, data_list) @cli.register_custom_action("ProjectMilestone") @exc.on_http_error(exc.GitlabListError) - def merge_requests(self, **kwargs): + async def merge_requests(self, **kwargs): """List the merge requests related to this milestone. Args: @@ -3170,7 +3178,7 @@ def merge_requests(self, **kwargs): RESTObjectList: The list of merge requests """ path = "%s/%s/merge_requests" % (self.manager.path, self.get_id()) - data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + data_list = await self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectMergeRequestManager( self.manager.gitlab, parent=self.manager._parent ) @@ -3198,7 +3206,7 @@ class ProjectLabel(SubscribableMixin, SaveMixin, ObjectDeleteMixin, RESTObject): # Update without ID, but we need an ID to get from list. @exc.on_http_error(exc.GitlabUpdateError) - def save(self, **kwargs): + async def save(self, **kwargs): """Saves the changes made to the object to the server. The object is updated to match what the server returns. @@ -3227,7 +3235,7 @@ class ProjectLabelManager( _update_attrs = (("name",), ("new_name", "color", "description", "priority")) # Update without ID. - def update(self, name, new_data=None, **kwargs): + async def update(self, name, new_data=None, **kwargs): """Update a Label on the server. Args: @@ -3241,7 +3249,7 @@ def update(self, name, new_data=None, **kwargs): # Delete without ID. @exc.on_http_error(exc.GitlabDeleteError) - def delete(self, name, **kwargs): + async def delete(self, name, **kwargs): """Delete a Label on the server. Args: @@ -3259,7 +3267,7 @@ class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "file_path" _short_print_attr = "file_path" - def decode(self): + async def decode(self): """Returns the decoded content of the file. Returns: @@ -3267,7 +3275,7 @@ def decode(self): """ return base64.b64decode(self.content) - def save(self, branch, commit_message, **kwargs): + async def save(self, branch, commit_message, **kwargs): """Save the changes made to the file to the server. The object is updated to match what the server returns. @@ -3286,7 +3294,7 @@ def save(self, branch, commit_message, **kwargs): self.file_path = self.file_path.replace("/", "%2F") super(ProjectFile, self).save(**kwargs) - def delete(self, branch, commit_message, **kwargs): + async def delete(self, branch, commit_message, **kwargs): """Delete the file from the server. Args: @@ -3316,7 +3324,7 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa ) @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) - def get(self, file_path, ref, **kwargs): + async def get(self, file_path, ref, **kwargs): """Retrieve a single file. Args: @@ -3340,7 +3348,7 @@ def get(self, file_path, ref, **kwargs): ("encoding", "author_email", "author_name"), ) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Create a new object. Args: @@ -3361,11 +3369,11 @@ def create(self, data, **kwargs): new_data = data.copy() file_path = new_data.pop("file_path").replace("/", "%2F") path = "%s/%s" % (self.path, file_path) - server_data = self.gitlab.http_post(path, post_data=new_data, **kwargs) + server_data = await self.gitlab.http_post(path, post_data=new_data, **kwargs) return self._obj_cls(self, server_data) @exc.on_http_error(exc.GitlabUpdateError) - def update(self, file_path, new_data=None, **kwargs): + async def update(self, file_path, new_data=None, **kwargs): """Update an object on the server. Args: @@ -3386,13 +3394,13 @@ def update(self, file_path, new_data=None, **kwargs): data["file_path"] = file_path path = "%s/%s" % (self.path, file_path) self._check_missing_update_attrs(data) - return self.gitlab.http_put(path, post_data=data, **kwargs) + return await self.gitlab.http_put(path, post_data=data, **kwargs) @cli.register_custom_action( "ProjectFileManager", ("file_path", "branch", "commit_message") ) @exc.on_http_error(exc.GitlabDeleteError) - def delete(self, file_path, branch, commit_message, **kwargs): + async def delete(self, file_path, branch, commit_message, **kwargs): """Delete a file on the server. Args: @@ -3411,7 +3419,7 @@ def delete(self, file_path, branch, commit_message, **kwargs): @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) @exc.on_http_error(exc.GitlabGetError) - def raw( + async def raw( self, file_path, ref, streamed=False, action=None, chunk_size=1024, **kwargs ): """Return the content of a file for a commit. @@ -3444,7 +3452,7 @@ def raw( @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) @exc.on_http_error(exc.GitlabListError) - def blame(self, file_path, ref, **kwargs): + async def blame(self, file_path, ref, **kwargs): """Return the content of a file for a commit. Args: @@ -3462,7 +3470,7 @@ def blame(self, file_path, ref, **kwargs): file_path = file_path.replace("/", "%2F").replace(".", "%2E") path = "%s/%s/blame" % (self.path, file_path) query_data = {"ref": ref} - return self.gitlab.http_list(path, query_data, **kwargs) + return await self.gitlab.http_list(path, query_data, **kwargs) class ProjectPipelineJob(RESTObject): @@ -3494,7 +3502,7 @@ class ProjectPipeline(RESTObject, RefreshMixin, ObjectDeleteMixin): @cli.register_custom_action("ProjectPipeline") @exc.on_http_error(exc.GitlabPipelineCancelError) - def cancel(self, **kwargs): + async def cancel(self, **kwargs): """Cancel the job. Args: @@ -3505,11 +3513,11 @@ def cancel(self, **kwargs): GitlabPipelineCancelError: If the request failed """ path = "%s/%s/cancel" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path) + await self.manager.gitlab.http_post(path) @cli.register_custom_action("ProjectPipeline") @exc.on_http_error(exc.GitlabPipelineRetryError) - def retry(self, **kwargs): + async def retry(self, **kwargs): """Retry the job. Args: @@ -3520,7 +3528,7 @@ def retry(self, **kwargs): GitlabPipelineRetryError: If the request failed """ path = "%s/%s/retry" % (self.manager.path, self.get_id()) - self.manager.gitlab.http_post(path) + await self.manager.gitlab.http_post(path) class ProjectPipelineManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): @@ -3540,7 +3548,7 @@ class ProjectPipelineManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManage ) _create_attrs = (("ref",), tuple()) - def create(self, data, **kwargs): + async def create(self, data, **kwargs): """Creates a new object. Args: @@ -3582,7 +3590,7 @@ class ProjectPipelineSchedule(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("ProjectPipelineSchedule") @exc.on_http_error(exc.GitlabOwnershipError) - def take_ownership(self, **kwargs): + async def take_ownership(self, **kwargs): """Update the owner of a pipeline schedule. Args: @@ -3593,7 +3601,7 @@ def take_ownership(self, **kwargs): GitlabOwnershipError: If the request failed """ path = "%s/%s/take_ownership" % (self.manager.path, self.get_id()) - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @@ -3727,7 +3735,7 @@ class ProjectSnippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObj @cli.register_custom_action("ProjectSnippet") @exc.on_http_error(exc.GitlabGetError) - def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): + async def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the content of a snippet. Args: @@ -3747,7 +3755,7 @@ def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): str: The snippet content """ path = "%s/%s/raw" % (self.manager.path, self.get_id()) - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @@ -3767,7 +3775,7 @@ class ProjectSnippetManager(CRUDMixin, RESTManager): class ProjectTrigger(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("ProjectTrigger") @exc.on_http_error(exc.GitlabOwnershipError) - def take_ownership(self, **kwargs): + async def take_ownership(self, **kwargs): """Update the owner of a trigger. Args: @@ -3778,7 +3786,7 @@ def take_ownership(self, **kwargs): GitlabOwnershipError: If the request failed """ path = "%s/%s/take_ownership" % (self.manager.path, self.get_id()) - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @@ -3871,7 +3879,7 @@ class ProjectServiceManager(GetMixin, UpdateMixin, DeleteMixin, RESTManager): "teamcity": (("teamcity_url", "build_type", "username", "password"), tuple()), } - def get(self, id, **kwargs): + async def get(self, id, **kwargs): """Retrieve a single object. Args: @@ -3892,7 +3900,7 @@ def get(self, id, **kwargs): obj.id = id return obj - def update(self, id=None, new_data=None, **kwargs): + async def update(self, id=None, new_data=None, **kwargs): """Update an object on the server. Args: @@ -3912,7 +3920,7 @@ def update(self, id=None, new_data=None, **kwargs): self.id = id @cli.register_custom_action("ProjectServiceManager") - def available(self, **kwargs): + async def available(self, **kwargs): """List the services known by python-gitlab. Returns: @@ -3952,7 +3960,7 @@ class ProjectApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _update_uses_post = True @exc.on_http_error(exc.GitlabUpdateError) - def set_approvers(self, approver_ids=None, approver_group_ids=None, **kwargs): + async def set_approvers(self, approver_ids=None, approver_group_ids=None, **kwargs): """Change project-level allowed approvers and approver groups. Args: @@ -4047,7 +4055,7 @@ class ProjectExport(RefreshMixin, RESTObject): @cli.register_custom_action("ProjectExport") @exc.on_http_error(exc.GitlabGetError) - def download(self, streamed=False, action=None, chunk_size=1024, **kwargs): + async def download(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Download the archive of a project export. Args: @@ -4067,7 +4075,7 @@ def download(self, streamed=False, action=None, chunk_size=1024, **kwargs): str: The blob content if streamed is False, None otherwise """ path = "/projects/%s/export/download" % self.project_id - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @@ -4161,7 +4169,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("Project", ("submodule", "branch", "commit_sha")) @exc.on_http_error(exc.GitlabUpdateError) - def update_submodule(self, submodule, branch, commit_sha, **kwargs): + async def update_submodule(self, submodule, branch, commit_sha, **kwargs): """Update a project submodule Args: @@ -4180,11 +4188,11 @@ def update_submodule(self, submodule, branch, commit_sha, **kwargs): data = {"branch": branch, "commit_sha": commit_sha} if "commit_message" in kwargs: data["commit_message"] = kwargs["commit_message"] - return self.manager.gitlab.http_put(path, post_data=data) + return await self.manager.gitlab.http_put(path, post_data=data) @cli.register_custom_action("Project", tuple(), ("path", "ref", "recursive")) @exc.on_http_error(exc.GitlabGetError) - def repository_tree(self, path="", ref="", recursive=False, **kwargs): + async def repository_tree(self, path="", ref="", recursive=False, **kwargs): """Return a list of files in the repository. Args: @@ -4211,11 +4219,13 @@ def repository_tree(self, path="", ref="", recursive=False, **kwargs): query_data["path"] = path if ref: query_data["ref"] = ref - return self.manager.gitlab.http_list(gl_path, query_data=query_data, **kwargs) + return await self.manager.gitlab.http_list( + gl_path, query_data=query_data, **kwargs + ) @cli.register_custom_action("Project", ("sha",)) @exc.on_http_error(exc.GitlabGetError) - def repository_blob(self, sha, **kwargs): + async def repository_blob(self, sha, **kwargs): """Return a file by blob SHA. Args: @@ -4231,11 +4241,11 @@ def repository_blob(self, sha, **kwargs): """ path = "/projects/%s/repository/blobs/%s" % (self.get_id(), sha) - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("Project", ("sha",)) @exc.on_http_error(exc.GitlabGetError) - def repository_raw_blob( + async def repository_raw_blob( self, sha, streamed=False, action=None, chunk_size=1024, **kwargs ): """Return the raw file contents for a blob. @@ -4258,14 +4268,14 @@ def repository_raw_blob( str: The blob content if streamed is False, None otherwise """ path = "/projects/%s/repository/blobs/%s/raw" % (self.get_id(), sha) - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("Project", ("from_", "to")) @exc.on_http_error(exc.GitlabGetError) - def repository_compare(self, from_, to, **kwargs): + async def repository_compare(self, from_, to, **kwargs): """Return a diff between two branches/commits. Args: @@ -4282,11 +4292,11 @@ def repository_compare(self, from_, to, **kwargs): """ path = "/projects/%s/repository/compare" % self.get_id() query_data = {"from": from_, "to": to} - return self.manager.gitlab.http_get(path, query_data=query_data, **kwargs) + return await self.manager.gitlab.http_get(path, query_data=query_data, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabGetError) - def repository_contributors(self, **kwargs): + async def repository_contributors(self, **kwargs): """Return a list of contributors for the project. Args: @@ -4305,11 +4315,11 @@ def repository_contributors(self, **kwargs): list: The contributors """ path = "/projects/%s/repository/contributors" % self.get_id() - return self.manager.gitlab.http_list(path, **kwargs) + return await self.manager.gitlab.http_list(path, **kwargs) @cli.register_custom_action("Project", tuple(), ("sha",)) @exc.on_http_error(exc.GitlabListError) - def repository_archive( + async def repository_archive( self, sha=None, streamed=False, action=None, chunk_size=1024, **kwargs ): """Return a tarball of the repository. @@ -4335,14 +4345,14 @@ def repository_archive( query_data = {} if sha: query_data["sha"] = sha - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, query_data=query_data, raw=True, streamed=streamed, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("Project", ("forked_from_id",)) @exc.on_http_error(exc.GitlabCreateError) - def create_fork_relation(self, forked_from_id, **kwargs): + async def create_fork_relation(self, forked_from_id, **kwargs): """Create a forked from/to relation between existing projects. Args: @@ -4354,11 +4364,11 @@ def create_fork_relation(self, forked_from_id, **kwargs): GitlabCreateError: If the relation could not be created """ path = "/projects/%s/fork/%s" % (self.get_id(), forked_from_id) - self.manager.gitlab.http_post(path, **kwargs) + await self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabDeleteError) - def delete_fork_relation(self, **kwargs): + async def delete_fork_relation(self, **kwargs): """Delete a forked relation between existing projects. Args: @@ -4369,11 +4379,11 @@ def delete_fork_relation(self, **kwargs): GitlabDeleteError: If the server failed to perform the request """ path = "/projects/%s/fork" % self.get_id() - self.manager.gitlab.http_delete(path, **kwargs) + await self.manager.gitlab.http_delete(path, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabDeleteError) - def delete_merged_branches(self, **kwargs): + async def delete_merged_branches(self, **kwargs): """Delete merged branches. Args: @@ -4384,11 +4394,11 @@ def delete_merged_branches(self, **kwargs): GitlabDeleteError: If the server failed to perform the request """ path = "/projects/%s/repository/merged_branches" % self.get_id() - self.manager.gitlab.http_delete(path, **kwargs) + await self.manager.gitlab.http_delete(path, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabGetError) - def languages(self, **kwargs): + async def languages(self, **kwargs): """Get languages used in the project with percentage value. Args: @@ -4399,11 +4409,11 @@ def languages(self, **kwargs): GitlabGetError: If the server failed to perform the request """ path = "/projects/%s/languages" % self.get_id() - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabCreateError) - def star(self, **kwargs): + async def star(self, **kwargs): """Star a project. Args: @@ -4414,12 +4424,12 @@ def star(self, **kwargs): GitlabCreateError: If the server failed to perform the request """ path = "/projects/%s/star" % self.get_id() - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabDeleteError) - def unstar(self, **kwargs): + async def unstar(self, **kwargs): """Unstar a project. Args: @@ -4430,12 +4440,12 @@ def unstar(self, **kwargs): GitlabDeleteError: If the server failed to perform the request """ path = "/projects/%s/unstar" % self.get_id() - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabCreateError) - def archive(self, **kwargs): + async def archive(self, **kwargs): """Archive a project. Args: @@ -4446,12 +4456,12 @@ def archive(self, **kwargs): GitlabCreateError: If the server failed to perform the request """ path = "/projects/%s/archive" % self.get_id() - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabDeleteError) - def unarchive(self, **kwargs): + async def unarchive(self, **kwargs): """Unarchive a project. Args: @@ -4462,14 +4472,14 @@ def unarchive(self, **kwargs): GitlabDeleteError: If the server failed to perform the request """ path = "/projects/%s/unarchive" % self.get_id() - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action( "Project", ("group_id", "group_access"), ("expires_at",) ) @exc.on_http_error(exc.GitlabCreateError) - def share(self, group_id, group_access, expires_at=None, **kwargs): + async def share(self, group_id, group_access, expires_at=None, **kwargs): """Share the project with a group. Args: @@ -4487,11 +4497,11 @@ def share(self, group_id, group_access, expires_at=None, **kwargs): "group_access": group_access, "expires_at": expires_at, } - self.manager.gitlab.http_post(path, post_data=data, **kwargs) + await self.manager.gitlab.http_post(path, post_data=data, **kwargs) @cli.register_custom_action("Project", ("group_id",)) @exc.on_http_error(exc.GitlabDeleteError) - def unshare(self, group_id, **kwargs): + async def unshare(self, group_id, **kwargs): """Delete a shared project link within a group. Args: @@ -4503,12 +4513,12 @@ def unshare(self, group_id, **kwargs): GitlabDeleteError: If the server failed to perform the request """ path = "/projects/%s/share/%s" % (self.get_id(), group_id) - self.manager.gitlab.http_delete(path, **kwargs) + await self.manager.gitlab.http_delete(path, **kwargs) # variables not supported in CLI @cli.register_custom_action("Project", ("ref", "token")) @exc.on_http_error(exc.GitlabCreateError) - def trigger_pipeline(self, ref, token, variables=None, **kwargs): + async def trigger_pipeline(self, ref, token, variables=None, **kwargs): """Trigger a CI build. See https://gitlab.com/help/ci/triggers/README.md#trigger-a-build @@ -4526,12 +4536,12 @@ def trigger_pipeline(self, ref, token, variables=None, **kwargs): variables = variables or {} path = "/projects/%s/trigger/pipeline" % self.get_id() post_data = {"ref": ref, "token": token, "variables": variables} - attrs = self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + attrs = await self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) return ProjectPipeline(self.pipelines, attrs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabHousekeepingError) - def housekeeping(self, **kwargs): + async def housekeeping(self, **kwargs): """Start the housekeeping task. Args: @@ -4543,12 +4553,12 @@ def housekeeping(self, **kwargs): request """ path = "/projects/%s/housekeeping" % self.get_id() - self.manager.gitlab.http_post(path, **kwargs) + await self.manager.gitlab.http_post(path, **kwargs) # see #56 - add file attachment features @cli.register_custom_action("Project", ("filename", "filepath")) @exc.on_http_error(exc.GitlabUploadError) - def upload(self, filename, filedata=None, filepath=None, **kwargs): + async def upload(self, filename, filedata=None, filepath=None, **kwargs): """Upload the specified file into the project. .. note:: @@ -4586,13 +4596,13 @@ def upload(self, filename, filedata=None, filepath=None, **kwargs): url = "/projects/%(id)s/uploads" % {"id": self.id} file_info = {"file": (filename, filedata)} - data = self.manager.gitlab.http_post(url, files=file_info) + data = await self.manager.gitlab.http_post(url, files=file_info) return {"alt": data["alt"], "url": data["url"], "markdown": data["markdown"]} @cli.register_custom_action("Project", optional=("wiki",)) @exc.on_http_error(exc.GitlabGetError) - def snapshot( + async def snapshot( self, wiki=False, streamed=False, action=None, chunk_size=1024, **kwargs ): """Return a snapshot of the repository. @@ -4615,14 +4625,14 @@ def snapshot( str: The uncompressed tar archive of the repository """ path = "/projects/%s/snapshot" % self.get_id() - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("Project", ("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) - def search(self, scope, search, **kwargs): + async def search(self, scope, search, **kwargs): """Search the project resources matching the provided string.' Args: @@ -4639,11 +4649,11 @@ def search(self, scope, search, **kwargs): """ data = {"scope": scope, "search": search} path = "/projects/%s/search" % self.get_id() - return self.manager.gitlab.http_list(path, query_data=data, **kwargs) + return await self.manager.gitlab.http_list(path, query_data=data, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabCreateError) - def mirror_pull(self, **kwargs): + async def mirror_pull(self, **kwargs): """Start the pull mirroring process for the project. Args: @@ -4654,11 +4664,11 @@ def mirror_pull(self, **kwargs): GitlabCreateError: If the server failed to perform the request """ path = "/projects/%s/mirror/pull" % self.get_id() - self.manager.gitlab.http_post(path, **kwargs) + await self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action("Project", ("to_namespace",)) @exc.on_http_error(exc.GitlabTransferProjectError) - def transfer_project(self, to_namespace, **kwargs): + async def transfer_project(self, to_namespace, **kwargs): """Transfer a project to the given namespace ID Args: @@ -4671,13 +4681,13 @@ def transfer_project(self, to_namespace, **kwargs): GitlabTransferProjectError: If the project could not be transfered """ path = "/projects/%s/transfer" % (self.id,) - self.manager.gitlab.http_put( + await self.manager.gitlab.http_put( path, post_data={"namespace": to_namespace}, **kwargs ) @cli.register_custom_action("Project", ("ref_name", "artifact_path", "job")) @exc.on_http_error(exc.GitlabGetError) - def artifact( + async def artifact( self, ref_name, artifact_path, @@ -4715,7 +4725,7 @@ def artifact( artifact_path, job, ) - result = self.manager.gitlab.http_get( + result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @@ -4798,7 +4808,7 @@ class ProjectManager(CRUDMixin, RESTManager): "with_custom_attributes", ) - def import_project( + async def import_project( self, file, path, @@ -4833,11 +4843,11 @@ def import_project( data["override_params[%s]" % k] = v if namespace: data["namespace"] = namespace - return self.gitlab.http_post( + return await self.gitlab.http_post( "/projects/import", post_data=data, files=files, **kwargs ) - def import_github( + async def import_github( self, personal_access_token, repo_id, target_namespace, new_name=None, **kwargs ): """Import a project from Github to Gitlab (schedule the import) @@ -4897,7 +4907,7 @@ def import_github( # and this is too short for this API command, typically. # On the order of 24 seconds has been measured on a typical gitlab instance. kwargs["timeout"] = 60.0 - result = self.gitlab.http_post("/import/github", post_data=data, **kwargs) + result = await self.gitlab.http_post("/import/github", post_data=data, **kwargs) return result @@ -4947,7 +4957,7 @@ class RunnerManager(CRUDMixin, RESTManager): @cli.register_custom_action("RunnerManager", tuple(), ("scope",)) @exc.on_http_error(exc.GitlabListError) - def all(self, scope=None, **kwargs): + async def all(self, scope=None, **kwargs): """List all the runners. Args: @@ -4971,11 +4981,11 @@ def all(self, scope=None, **kwargs): query_data = {} if scope is not None: query_data["scope"] = scope - return self.gitlab.http_list(path, query_data, **kwargs) + return await self.gitlab.http_list(path, query_data, **kwargs) @cli.register_custom_action("RunnerManager", ("token",)) @exc.on_http_error(exc.GitlabVerifyError) - def verify(self, token, **kwargs): + async def verify(self, token, **kwargs): """Validates authentication credentials for a registered Runner. Args: @@ -4988,13 +4998,13 @@ def verify(self, token, **kwargs): """ path = "/runners/verify" post_data = {"token": token} - self.gitlab.http_post(path, post_data=post_data, **kwargs) + await self.gitlab.http_post(path, post_data=post_data, **kwargs) class Todo(ObjectDeleteMixin, RESTObject): @cli.register_custom_action("Todo") @exc.on_http_error(exc.GitlabTodoError) - def mark_as_done(self, **kwargs): + async def mark_as_done(self, **kwargs): """Mark the todo as done. Args: @@ -5005,7 +5015,7 @@ def mark_as_done(self, **kwargs): GitlabTodoError: If the server failed to perform the request """ path = "%s/%s/mark_as_done" % (self.manager.path, self.id) - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @@ -5016,7 +5026,7 @@ class TodoManager(ListMixin, DeleteMixin, RESTManager): @cli.register_custom_action("TodoManager") @exc.on_http_error(exc.GitlabTodoError) - def mark_all_as_done(self, **kwargs): + async def mark_all_as_done(self, **kwargs): """Mark all the todos as done. Args: @@ -5029,13 +5039,13 @@ def mark_all_as_done(self, **kwargs): Returns: int: The number of todos maked done """ - result = self.gitlab.http_post("/todos/mark_as_done", **kwargs) + result = await self.gitlab.http_post("/todos/mark_as_done", **kwargs) class GeoNode(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("GeoNode") @exc.on_http_error(exc.GitlabRepairError) - def repair(self, **kwargs): + async def repair(self, **kwargs): """Repair the OAuth authentication of the geo node. Args: @@ -5046,12 +5056,12 @@ def repair(self, **kwargs): GitlabRepairError: If the server failed to perform the request """ path = "/geo_nodes/%s/repair" % self.get_id() - server_data = self.manager.gitlab.http_post(path, **kwargs) + server_data = await self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("GeoNode") @exc.on_http_error(exc.GitlabGetError) - def status(self, **kwargs): + async def status(self, **kwargs): """Get the status of the geo node. Args: @@ -5065,7 +5075,7 @@ def status(self, **kwargs): dict: The status of the geo node """ path = "/geo_nodes/%s/status" % self.get_id() - return self.manager.gitlab.http_get(path, **kwargs) + return await self.manager.gitlab.http_get(path, **kwargs) class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): @@ -5078,7 +5088,7 @@ class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): @cli.register_custom_action("GeoNodeManager") @exc.on_http_error(exc.GitlabGetError) - def status(self, **kwargs): + async def status(self, **kwargs): """Get the status of all the geo nodes. Args: @@ -5091,11 +5101,11 @@ def status(self, **kwargs): Returns: list: The status of all the geo nodes """ - return self.gitlab.http_list("/geo_nodes/status", **kwargs) + return await self.gitlab.http_list("/geo_nodes/status", **kwargs) @cli.register_custom_action("GeoNodeManager") @exc.on_http_error(exc.GitlabGetError) - def current_failures(self, **kwargs): + async def current_failures(self, **kwargs): """Get the list of failures on the current geo node. Args: @@ -5108,4 +5118,4 @@ def current_failures(self, **kwargs): Returns: list: The list of failures """ - return self.gitlab.http_list("/geo_nodes/current/failures", **kwargs) + return await self.gitlab.http_list("/geo_nodes/current/failures", **kwargs) diff --git a/requirements.txt b/requirements.txt index d5c2bc9c6..54876157b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests>=2.22.0 +httpx>=0.11.1,<0.12 diff --git a/setup.py b/setup.py index 6b5737300..06f444fc2 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from setuptools import setup -from setuptools import find_packages +from setuptools import find_packages, setup def get_version(): @@ -25,7 +24,7 @@ def get_version(): license="LGPLv3", url="https://github.com/python-gitlab/python-gitlab", packages=find_packages(), - install_requires=["requests>=2.22.0"], + install_requires=["httpx>=0.11.1,<0.12"], python_requires=">=3.6.0", entry_points={"console_scripts": ["gitlab = gitlab.cli:main"]}, classifiers=[ diff --git a/test-requirements.txt b/test-requirements.txt index 65d09d7d3..5ea2d9364 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,3 +7,4 @@ jinja2 mock sphinx>=1.3 sphinx_rtd_theme +requests>=2.22.0 From fb90a89e6dbe7bec1b205229965d2b0a66d499a1 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Wed, 19 Feb 2020 09:02:06 +0300 Subject: [PATCH 02/15] test: create TestGitlabList for async way --- gitlab/__init__.py | 22 ++++++--- gitlab/tests/test_async_gitlab.py | 79 +++++++++++++++++++++++++++++++ test-requirements.txt | 1 + 3 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 gitlab/tests/test_async_gitlab.py diff --git a/gitlab/__init__.py b/gitlab/__init__.py index a2a860e67..056846a48 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -22,8 +22,9 @@ import importlib import warnings -import gitlab.config import httpx + +import gitlab.config from gitlab import utils # noqa from gitlab.const import * # noqa from gitlab.exceptions import * # noqa @@ -622,13 +623,15 @@ async def http_list(self, path, query_data=None, as_list=None, **kwargs): url = self._build_url(path) if get_all is True and as_list is True: - return list(await GitlabList.create(self, url, query_data, **kwargs)) + gitlab_list = await GitlabList.create(self, url, query_data, **kwargs) + return await gitlab_list.as_list() if "page" in kwargs or as_list is True: # pagination requested, we return a list - return list( - await GitlabList.create(self, url, query_data, get_next=False, **kwargs) + gitlab_list = await GitlabList.create( + self, url, query_data, get_next=False, **kwargs ) + return await gitlab_list.as_list() # No pagination, generator requested return await GitlabList.create(self, url, query_data, **kwargs) @@ -825,10 +828,13 @@ def total(self): """The total number of items.""" return int(self._total) - async def __aiter__(self): - return await self + def __aiter__(self): + return self async def __anext__(self): + return await self.next() + + async def next(self): try: item = self._data[self._current] self._current += 1 @@ -844,3 +850,7 @@ async def __anext__(self): def __len__(self): return int(self._total) + + async def as_list(self): + # since list() does not support async way + return [o async for o in self] diff --git a/gitlab/tests/test_async_gitlab.py b/gitlab/tests/test_async_gitlab.py new file mode 100644 index 000000000..0a99bf1af --- /dev/null +++ b/gitlab/tests/test_async_gitlab.py @@ -0,0 +1,79 @@ +import pytest +import respx +from httpx import status_codes +from httpx.status_codes import StatusCode + +from gitlab import Gitlab, GitlabList + + +class TestGitlabList: + @pytest.fixture + def gl(self): + return Gitlab("http://localhost", private_token="private_token", api_version=4) + + @respx.mock + @pytest.mark.asyncio + async def test_build_list(self, gl): + request_1 = respx.get( + "http://localhost/api/v4/tests", + headers={ + "content-type": "application/json", + "X-Page": "1", + "X-Next-Page": "2", + "X-Per-Page": "1", + "X-Total-Pages": "2", + "X-Total": "2", + "Link": ( + ";" ' rel="next"' + ), + }, + content=[{"a": "b"}], + status_code=StatusCode.OK, + ) + request_2 = respx.get( + "http://localhost/api/v4/tests?per_page=1&page=2", + headers={ + "content-type": "application/json", + "X-Page": "2", + "X-Next-Page": "2", + "X-Per-Page": "1", + "X-Total-Pages": "2", + "X-Total": "2", + }, + content=[{"c": "d"}], + status_code=StatusCode.OK, + ) + + obj = await gl.http_list("/tests", as_list=False) + assert len(obj) == 2 + assert obj._next_url == "http://localhost/api/v4/tests?per_page=1&page=2" + assert obj.current_page == 1 + assert obj.prev_page == None + assert obj.next_page == 2 + assert obj.per_page == 1 + assert obj.total_pages == 2 + assert obj.total == 2 + + l = await obj.as_list() + assert len(l) == 2 + assert l[0]["a"] == "b" + assert l[1]["c"] == "d" + + @respx.mock + @pytest.mark.asyncio + async def test_all_ommited_when_as_list(self, gl): + request = respx.get( + "http://localhost/api/v4/tests", + headers={ + "content-type": "application/json", + "X-Page": "2", + "X-Next-Page": "2", + "X-Per-Page": "1", + "X-Total-Pages": "2", + "X-Total": "2", + }, + content=[{"c": "d"}], + status_code=StatusCode.OK, + ) + result = await gl.http_list("/tests", as_list=False, all=True) + assert isinstance(result, GitlabList) diff --git a/test-requirements.txt b/test-requirements.txt index 5ea2d9364..d51717541 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,3 +8,4 @@ mock sphinx>=1.3 sphinx_rtd_theme requests>=2.22.0 +respx>=0.10.0,<0.11 From 3b90d09059362d33b3b9675d4c276dd2eada52b8 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Wed, 19 Feb 2020 09:08:48 +0300 Subject: [PATCH 03/15] test: add pytest test dependency and remove sync TestGitlabList --- gitlab/tests/test_async_gitlab.py | 1 - gitlab/tests/test_gitlab.py | 85 +------------------------------ test-requirements.txt | 1 + 3 files changed, 3 insertions(+), 84 deletions(-) diff --git a/gitlab/tests/test_async_gitlab.py b/gitlab/tests/test_async_gitlab.py index 0a99bf1af..01b178831 100644 --- a/gitlab/tests/test_async_gitlab.py +++ b/gitlab/tests/test_async_gitlab.py @@ -1,6 +1,5 @@ import pytest import respx -from httpx import status_codes from httpx.status_codes import StatusCode from gitlab import Gitlab, GitlabList diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 3eccf6e7d..b30e6fe11 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -16,22 +16,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +import json import os import pickle import tempfile -import json import unittest +import requests from httmock import HTTMock # noqa from httmock import response # noqa from httmock import urlmatch # noqa -import requests import gitlab from gitlab import * # noqa from gitlab.v4.objects import * # noqa - valid_config = b"""[global] default = one ssl_verify = true @@ -58,86 +57,6 @@ def test_dict(self): self.assertEqual(expected, gitlab._sanitize(source)) -class TestGitlabList(unittest.TestCase): - def setUp(self): - self.gl = Gitlab( - "http://localhost", private_token="private_token", api_version=4 - ) - - def test_build_list(self): - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") - def resp_1(url, request): - headers = { - "content-type": "application/json", - "X-Page": 1, - "X-Next-Page": 2, - "X-Per-Page": 1, - "X-Total-Pages": 2, - "X-Total": 2, - "Link": ( - ";" ' rel="next"' - ), - } - content = '[{"a": "b"}]' - return response(200, content, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/tests", - method="get", - query=r".*page=2", - ) - def resp_2(url, request): - headers = { - "content-type": "application/json", - "X-Page": 2, - "X-Next-Page": 2, - "X-Per-Page": 1, - "X-Total-Pages": 2, - "X-Total": 2, - } - content = '[{"c": "d"}]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_1): - obj = self.gl.http_list("/tests", as_list=False) - self.assertEqual(len(obj), 2) - self.assertEqual( - obj._next_url, "http://localhost/api/v4/tests?per_page=1&page=2" - ) - self.assertEqual(obj.current_page, 1) - self.assertEqual(obj.prev_page, None) - self.assertEqual(obj.next_page, 2) - self.assertEqual(obj.per_page, 1) - self.assertEqual(obj.total_pages, 2) - self.assertEqual(obj.total, 2) - - with HTTMock(resp_2): - l = list(obj) - self.assertEqual(len(l), 2) - self.assertEqual(l[0]["a"], "b") - self.assertEqual(l[1]["c"], "d") - - def test_all_omitted_when_as_list(self): - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") - def resp(url, request): - headers = { - "content-type": "application/json", - "X-Page": 2, - "X-Next-Page": 2, - "X-Per-Page": 1, - "X-Total-Pages": 2, - "X-Total": 2, - } - content = '[{"c": "d"}]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp): - result = self.gl.http_list("/tests", as_list=False, all=True) - self.assertIsInstance(result, GitlabList) - - class TestGitlabHttpMethods(unittest.TestCase): def setUp(self): self.gl = Gitlab( diff --git a/test-requirements.txt b/test-requirements.txt index d51717541..15ec246cf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,3 +9,4 @@ sphinx>=1.3 sphinx_rtd_theme requests>=2.22.0 respx>=0.10.0,<0.11 +pytest From d9d9af807644e3e8a79e607762cd91a70f17b450 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Wed, 19 Feb 2020 12:18:31 +0300 Subject: [PATCH 04/15] test: refactor TestGitlabHttpMethods to async way --- gitlab/tests/test_async_gitlab.py | 166 ++++++++++++++++++++++ gitlab/tests/test_gitlab.py | 223 ------------------------------ test-requirements.txt | 1 + 3 files changed, 167 insertions(+), 223 deletions(-) diff --git a/gitlab/tests/test_async_gitlab.py b/gitlab/tests/test_async_gitlab.py index 01b178831..70d67b785 100644 --- a/gitlab/tests/test_async_gitlab.py +++ b/gitlab/tests/test_async_gitlab.py @@ -1,8 +1,10 @@ +import httpx import pytest import respx from httpx.status_codes import StatusCode from gitlab import Gitlab, GitlabList +from gitlab import exceptions as exc class TestGitlabList: @@ -74,5 +76,169 @@ async def test_all_ommited_when_as_list(self, gl): content=[{"c": "d"}], status_code=StatusCode.OK, ) + result = await gl.http_list("/tests", as_list=False, all=True) assert isinstance(result, GitlabList) + + +class TestGitlabHttpMethods: + @pytest.fixture + def gl(self): + return Gitlab("http://localhost", private_token="private_token", api_version=4) + + @respx.mock + @pytest.mark.asyncio + async def test_http_request(self, gl): + request = respx.get( + "http://localhost/api/v4/projects", + headers={"content-type": "application/json"}, + content=[{"name": "project1"}], + status_code=StatusCode.OK, + ) + + http_r = await gl.http_request("get", "/projects") + http_r.json() + assert http_r.status_code == StatusCode.OK + + @respx.mock + @pytest.mark.asyncio + async def test_get_request(self, gl): + request = respx.get( + "http://localhost/api/v4/projects", + headers={"content-type": "application/json"}, + content={"name": "project1"}, + status_code=StatusCode.OK, + ) + + result = await gl.http_get("/projects") + assert isinstance(result, dict) + assert result["name"] == "project1" + + @respx.mock + @pytest.mark.asyncio + async def test_get_request_raw(self, gl): + request = respx.get( + "http://localhost/api/v4/projects", + headers={"content-type": "application/octet-stream"}, + content="content", + status_code=StatusCode.OK, + ) + + result = await gl.http_get("/projects") + assert result.content.decode("utf-8") == "content" + + @respx.mock + @pytest.mark.asyncio + @pytest.mark.parametrize( + "respx_params, gl_exc, path", + [ + ( + { + "url": "http://localhost/api/v4/not_there", + "content": "Here is why it failed", + "status_code": StatusCode.NOT_FOUND, + }, + exc.GitlabHttpError, + "/not_there", + ), + ( + { + "url": "http://localhost/api/v4/projects", + "headers": {"content-type": "application/json"}, + "content": '["name": "project1"]', + "status_code": StatusCode.OK, + }, + exc.GitlabParsingError, + "/projects", + ), + ], + ) + @pytest.mark.parametrize( + "http_method, gl_method", + [ + ("get", "http_get"), + ("get", "http_list"), + ("post", "http_post"), + ("put", "http_put"), + ], + ) + async def test_errors(self, gl, http_method, gl_method, respx_params, gl_exc, path): + request = getattr(respx, http_method)(**respx_params) + + with pytest.raises(gl_exc): + http_r = await getattr(gl, gl_method)(path) + + @respx.mock + @pytest.mark.asyncio + async def test_list_request(self, gl): + request = respx.get( + "http://localhost/api/v4/projects", + headers={"content-type": "application/json", "X-Total": "1"}, + content=[{"name": "project1"}], + status_code=StatusCode.OK, + ) + + result = await gl.http_list("/projects", as_list=True) + assert isinstance(result, list) + assert len(result) == 1 + + result = await gl.http_list("/projects", as_list=False) + assert isinstance(result, GitlabList) + assert len(result) == 1 + + result = await gl.http_list("/projects", all=True) + assert isinstance(result, list) + assert len(result) == 1 + + @respx.mock + @pytest.mark.asyncio + async def test_post_request(self, gl): + request = respx.post( + "http://localhost/api/v4/projects", + headers={"content-type": "application/json"}, + content={"name": "project1"}, + status_code=StatusCode.OK, + ) + + result = await gl.http_post("/projects") + assert isinstance(result, dict) + assert result["name"] == "project1" + + @respx.mock + @pytest.mark.asyncio + async def test_put_request(self, gl): + request = respx.put( + "http://localhost/api/v4/projects", + headers={"content-type": "application/json"}, + content='{"name": "project1"}', + status_code=StatusCode.OK, + ) + result = await gl.http_put("/projects") + assert isinstance(result, dict) + assert result["name"] == "project1" + + @respx.mock + @pytest.mark.asyncio + async def test_delete_request(self, gl): + request = respx.delete( + "http://localhost/api/v4/projects", + headers={"content-type": "application/json"}, + content="true", + status_code=StatusCode.OK, + ) + + result = await gl.http_delete("/projects") + assert isinstance(result, httpx.Response) + assert result.json() is True + + @respx.mock + @pytest.mark.asyncio + async def test_delete_request_404(self, gl): + result = respx.delete( + "http://localhost/api/v4/not_there", + content="Here is why it failed", + status_code=StatusCode.NOT_FOUND, + ) + + with pytest.raises(exc.GitlabHttpError): + await gl.http_delete("/not_there") diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index b30e6fe11..523ba79e7 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -71,229 +71,6 @@ def test_build_url(self): r = self.gl._build_url("/projects") self.assertEqual(r, "http://localhost/api/v4/projects") - def test_http_request(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="get" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '[{"name": "project1"}]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - http_r = self.gl.http_request("get", "/projects") - http_r.json() - self.assertEqual(http_r.status_code, 200) - - def test_http_request_404(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/not_there", method="get" - ) - def resp_cont(url, request): - content = {"Here is wh it failed"} - return response(404, content, {}, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises( - GitlabHttpError, self.gl.http_request, "get", "/not_there" - ) - - def test_get_request(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="get" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "project1"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - result = self.gl.http_get("/projects") - self.assertIsInstance(result, dict) - self.assertEqual(result["name"], "project1") - - def test_get_request_raw(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="get" - ) - def resp_cont(url, request): - headers = {"content-type": "application/octet-stream"} - content = "content" - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - result = self.gl.http_get("/projects") - self.assertEqual(result.content.decode("utf-8"), "content") - - def test_get_request_404(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/not_there", method="get" - ) - def resp_cont(url, request): - content = {"Here is wh it failed"} - return response(404, content, {}, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabHttpError, self.gl.http_get, "/not_there") - - def test_get_request_invalid_data(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="get" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '["name": "project1"]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabParsingError, self.gl.http_get, "/projects") - - def test_list_request(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="get" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json", "X-Total": 1} - content = '[{"name": "project1"}]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - result = self.gl.http_list("/projects", as_list=True) - self.assertIsInstance(result, list) - self.assertEqual(len(result), 1) - - with HTTMock(resp_cont): - result = self.gl.http_list("/projects", as_list=False) - self.assertIsInstance(result, GitlabList) - self.assertEqual(len(result), 1) - - with HTTMock(resp_cont): - result = self.gl.http_list("/projects", all=True) - self.assertIsInstance(result, list) - self.assertEqual(len(result), 1) - - def test_list_request_404(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/not_there", method="get" - ) - def resp_cont(url, request): - content = {"Here is why it failed"} - return response(404, content, {}, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabHttpError, self.gl.http_list, "/not_there") - - def test_list_request_invalid_data(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="get" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '["name": "project1"]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabParsingError, self.gl.http_list, "/projects") - - def test_post_request(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="post" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "project1"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - result = self.gl.http_post("/projects") - self.assertIsInstance(result, dict) - self.assertEqual(result["name"], "project1") - - def test_post_request_404(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/not_there", method="post" - ) - def resp_cont(url, request): - content = {"Here is wh it failed"} - return response(404, content, {}, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabHttpError, self.gl.http_post, "/not_there") - - def test_post_request_invalid_data(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="post" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '["name": "project1"]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabParsingError, self.gl.http_post, "/projects") - - def test_put_request(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="put" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "project1"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - result = self.gl.http_put("/projects") - self.assertIsInstance(result, dict) - self.assertEqual(result["name"], "project1") - - def test_put_request_404(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/not_there", method="put" - ) - def resp_cont(url, request): - content = {"Here is wh it failed"} - return response(404, content, {}, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabHttpError, self.gl.http_put, "/not_there") - - def test_put_request_invalid_data(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="put" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '["name": "project1"]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabParsingError, self.gl.http_put, "/projects") - - def test_delete_request(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects", method="delete" - ) - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = "true" - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - result = self.gl.http_delete("/projects") - self.assertIsInstance(result, requests.Response) - self.assertEqual(result.json(), True) - - def test_delete_request_404(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/not_there", method="delete" - ) - def resp_cont(url, request): - content = {"Here is wh it failed"} - return response(404, content, {}, None, 5, request) - - with HTTMock(resp_cont): - self.assertRaises(GitlabHttpError, self.gl.http_delete, "/not_there") - class TestGitlabAuth(unittest.TestCase): def test_invalid_auth_args(self): diff --git a/test-requirements.txt b/test-requirements.txt index 15ec246cf..bbb8142ee 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,3 +10,4 @@ sphinx_rtd_theme requests>=2.22.0 respx>=0.10.0,<0.11 pytest +pytest-asyncio From c2614e2ad517302e9a7fe63047ea59312c261ce4 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Wed, 19 Feb 2020 13:04:07 +0300 Subject: [PATCH 05/15] test: fix TestGitlabAuth Change _http_auth on direct httpx.AsyncClient.auth property --- gitlab/tests/test_gitlab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 523ba79e7..b0f509da0 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -22,6 +22,7 @@ import tempfile import unittest +import httpx import requests from httmock import HTTMock # noqa from httmock import response # noqa @@ -113,7 +114,7 @@ def test_private_token_auth(self): self.assertEqual(gl.private_token, "private_token") self.assertEqual(gl.oauth_token, None) self.assertEqual(gl.job_token, None) - self.assertEqual(gl._http_auth, None) + self.assertEqual(gl.client.auth, None) self.assertNotIn("Authorization", gl.headers) self.assertEqual(gl.headers["PRIVATE-TOKEN"], "private_token") self.assertNotIn("JOB-TOKEN", gl.headers) @@ -123,7 +124,7 @@ def test_oauth_token_auth(self): self.assertEqual(gl.private_token, None) self.assertEqual(gl.oauth_token, "oauth_token") self.assertEqual(gl.job_token, None) - self.assertEqual(gl._http_auth, None) + self.assertEqual(gl.client.auth, None) self.assertEqual(gl.headers["Authorization"], "Bearer oauth_token") self.assertNotIn("PRIVATE-TOKEN", gl.headers) self.assertNotIn("JOB-TOKEN", gl.headers) @@ -133,7 +134,7 @@ def test_job_token_auth(self): self.assertEqual(gl.private_token, None) self.assertEqual(gl.oauth_token, None) self.assertEqual(gl.job_token, "CI_JOB_TOKEN") - self.assertEqual(gl._http_auth, None) + self.assertEqual(gl.client.auth, None) self.assertNotIn("Authorization", gl.headers) self.assertNotIn("PRIVATE-TOKEN", gl.headers) self.assertEqual(gl.headers["JOB-TOKEN"], "CI_JOB_TOKEN") @@ -149,7 +150,7 @@ def test_http_auth(self): self.assertEqual(gl.private_token, "private_token") self.assertEqual(gl.oauth_token, None) self.assertEqual(gl.job_token, None) - self.assertIsInstance(gl._http_auth, requests.auth.HTTPBasicAuth) + self.assertIsInstance(gl.client.auth, httpx.auth.BasicAuth) self.assertEqual(gl.headers["PRIVATE-TOKEN"], "private_token") self.assertNotIn("Authorization", gl.headers) From 38183a69c3f1feca361afe2054819c8005adbc5c Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Wed, 19 Feb 2020 14:31:35 +0300 Subject: [PATCH 06/15] test: mostly transfer TestGitlab to async path Fix some errors that are met on the way to async --- gitlab/__init__.py | 4 +- gitlab/mixins.py | 8 +- gitlab/tests/test_async_gitlab.py | 388 +++++++++++++++++++++++++++++ gitlab/tests/test_gitlab.py | 396 ------------------------------ 4 files changed, 394 insertions(+), 402 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 056846a48..624a94b14 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -221,13 +221,13 @@ def from_config(cls, gitlab_id=None, config_files=None): order_by=config.order_by, ) - def auth(self): + async def auth(self): """Performs an authentication using private token. The `user` attribute will hold a `gitlab.objects.CurrentUser` object on success. """ - self.user = self._objects.CurrentUserManager(self).get() + self.user = await self._objects.CurrentUserManager(self).get() def version(self): """Returns the version and revision of the gitlab server. diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 271119396..8df845772 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -46,8 +46,8 @@ async def get(self, id, lazy=False, **kwargs): path = "%s/%s" % (self.path, id) if lazy is True: return self._obj_cls(self, {self._obj_cls._id_attr: id}) - server_data = self.gitlab.http_get(path, **kwargs) - return await self._obj_cls(self, server_data) + server_data = await self.gitlab.http_get(path, **kwargs) + return self._obj_cls(self, server_data) class GetWithoutIdMixin(object): @@ -65,10 +65,10 @@ async def get(self, id=None, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ - server_data = self.gitlab.http_get(self.path, **kwargs) + server_data = await self.gitlab.http_get(self.path, **kwargs) if server_data is None: return None - return await self._obj_cls(self, server_data) + return self._obj_cls(self, server_data) class RefreshMixin(object): diff --git a/gitlab/tests/test_async_gitlab.py b/gitlab/tests/test_async_gitlab.py index 70d67b785..33aef72e3 100644 --- a/gitlab/tests/test_async_gitlab.py +++ b/gitlab/tests/test_async_gitlab.py @@ -1,3 +1,7 @@ +import json +import os +import re + import httpx import pytest import respx @@ -5,6 +9,18 @@ from gitlab import Gitlab, GitlabList from gitlab import exceptions as exc +from gitlab.v4.objects import ( + CurrentUser, + Group, + Hook, + Project, + ProjectAdditionalStatistics, + ProjectEnvironment, + ProjectIssuesStatistics, + Todo, + User, + UserStatus, +) class TestGitlabList: @@ -242,3 +258,375 @@ async def test_delete_request_404(self, gl): with pytest.raises(exc.GitlabHttpError): await gl.http_delete("/not_there") + + +class TestGitlab: + @pytest.fixture + def gl(self): + return Gitlab( + "http://localhost", + private_token="private_token", + ssl_verify=True, + api_version=4, + ) + + @respx.mock + @pytest.mark.asyncio + async def test_token_auth(self, gl): + name = "username" + id_ = 1 + + request = respx.get( + "http://localhost/api/v4/user", + headers={"content-type": "application/json"}, + content='{{"id": {0:d}, "username": "{1:s}"}}'.format(id_, name).encode( + "utf-8" + ), + status_code=StatusCode.OK, + ) + + await gl.auth() + assert gl.user.username == name + assert gl.user.id == id_ + assert isinstance(gl.user, CurrentUser) + + @respx.mock + @pytest.mark.asyncio + async def test_hooks(self, gl): + request = respx.get( + "http://localhost/api/v4/hooks/1", + headers={"content-type": "application/json"}, + content='{"url": "testurl", "id": 1}'.encode("utf-8"), + status_code=StatusCode.OK, + ) + + data = await gl.hooks.get(1) + assert isinstance(data, Hook) + assert data.url == "testurl" + assert data.id == 1 + + @respx.mock + @pytest.mark.asyncio + async def test_projects(self, gl): + request = respx.get( + "http://localhost/api/v4/projects/1", + headers={"content-type": "application/json"}, + content='{"name": "name", "id": 1}'.encode("utf-8"), + status_code=StatusCode.OK, + ) + + data = await gl.projects.get(1) + assert isinstance(data, Project) + assert data.name == "name" + assert data.id == 1 + + @respx.mock + @pytest.mark.asyncio + async def test_project_environments(self, gl): + request_get_project = respx.get( + "http://localhost/api/v4/projects/1", + headers={"content-type": "application/json"}, + content='{"name": "name", "id": 1}'.encode("utf-8"), + status_code=StatusCode.OK, + ) + request_get_environment = respx.get( + "http://localhost/api/v4/projects/1/environments/1", + headers={"content-type": "application/json"}, + content='{"name": "environment_name", "id": 1, "last_deployment": "sometime"}'.encode( + "utf-8" + ), + status_code=StatusCode.OK, + ) + + project = await gl.projects.get(1) + environment = await project.environments.get(1) + + assert isinstance(environment, ProjectEnvironment) + assert environment.id == 1 + assert environment.last_deployment == "sometime" + assert environment.name == "environment_name" + + @respx.mock + @pytest.mark.asyncio + async def test_project_additional_statistics(self, gl): + request_get_project = respx.get( + "http://localhost/api/v4/projects/1", + headers={"content-type": "application/json"}, + content='{"name": "name", "id": 1}'.encode("utf-8"), + status_code=StatusCode.OK, + ) + request_get_environment = respx.get( + "http://localhost/api/v4/projects/1/statistics", + headers={"content-type": "application/json"}, + content="""{"fetches": {"total": 50, "days": [{"count": 10, "date": "2018-01-10"}]}}""".encode( + "utf-8" + ), + status_code=StatusCode.OK, + ) + project = await gl.projects.get(1) + statistics = await project.additionalstatistics.get() + assert isinstance(statistics, ProjectAdditionalStatistics) + assert statistics.fetches["total"] == 50 + + @respx.mock + @pytest.mark.asyncio + async def test_project_issues_statistics(self, gl): + request_get_project = respx.get( + "http://localhost/api/v4/projects/1", + headers={"content-type": "application/json"}, + content='{"name": "name", "id": 1}'.encode("utf-8"), + status_code=StatusCode.OK, + ) + request_get_environment = respx.get( + "http://localhost/api/v4/projects/1/issues_statistics", + headers={"content-type": "application/json"}, + content="""{"statistics": {"counts": {"all": 20, "closed": 5, "opened": 15}}}""".encode( + "utf-8" + ), + status_code=StatusCode.OK, + ) + + project = await gl.projects.get(1) + statistics = await project.issuesstatistics.get() + + assert isinstance(statistics, ProjectIssuesStatistics) + assert statistics.statistics["counts"]["all"] == 20 + + @respx.mock + @pytest.mark.asyncio + async def test_groups(self, gl): + request = respx.get( + "http://localhost/api/v4/groups/1", + headers={"content-type": "application/json"}, + content='{"name": "name", "id": 1, "path": "path"}'.encode("utf-8"), + status_code=StatusCode.OK, + ) + + data = await gl.groups.get(1) + assert isinstance(data, Group) + assert data.name == "name" + assert data.path == "path" + assert data.id == 1 + + @respx.mock + @pytest.mark.asyncio + async def test_issues(self, gl): + request = respx.get( + "http://localhost/api/v4/issues", + headers={"content-type": "application/json"}, + content='[{"name": "name", "id": 1}, ' + '{"name": "other_name", "id": 2}]'.encode("utf-8"), + status_code=StatusCode.OK, + ) + + data = await gl.issues.list() + assert data[1].id == 2 + assert data[1].name == "other_name" + + @pytest.fixture + def respx_get_user_params(self): + return { + "url": "http://localhost/api/v4/users/1", + "headers": {"content-type": "application/json"}, + "content": ( + '{"name": "name", "id": 1, "password": "password", ' + '"username": "username", "email": "email"}'.encode("utf-8") + ), + "status_code": StatusCode.OK, + } + + @respx.mock + @pytest.mark.asyncio + async def test_users(self, gl, respx_get_user_params): + request = respx.get(**respx_get_user_params) + + user = await gl.users.get(1) + assert isinstance(user, User) + assert user.name == "name" + assert user.id == 1 + + @respx.mock + @pytest.mark.asyncio + async def test_user_status(self, gl, respx_get_user_params): + request_user_status = respx.get( + "http://localhost/api/v4/users/1/status", + headers={"content-type": "application/json"}, + content='{"message": "test", "message_html": "

Message

", "emoji": "thumbsup"}'.encode( + "utf-8" + ), + status_code=StatusCode.OK, + ) + request_user = respx.get(**respx_get_user_params) + + user = await gl.users.get(1) + status = await user.status.get() + assert isinstance(status, UserStatus) + assert status.message == "test" + assert status.emoji == "thumbsup" + + @respx.mock + @pytest.mark.asyncio + async def test_todo(self, gl): + with open(os.path.dirname(__file__) + "/data/todo.json", "r") as json_file: + todo_content = json_file.read() + json_content = json.loads(todo_content) + encoded_content = todo_content.encode("utf-8") + + request_get_todo = respx.get( + "http://localhost/api/v4/todos", + headers={"content-type": "application/json"}, + content=encoded_content, + status_code=StatusCode.OK, + ) + request_mark_as_done = respx.post( + "http://localhost/api/v4/todos/102/mark_as_done", + headers={"content-type": "application/json"}, + content=json.dumps(json_content[0]).encode("utf-8"), + status_code=StatusCode.OK, + ) + + todo = (await gl.todos.list())[0] + assert isinstance(todo, Todo) + assert todo.id == 102 + assert todo.target_type == "MergeRequest" + assert todo.target["assignee"]["username"] == "root" + await todo.mark_as_done() + + @respx.mock + @pytest.mark.asyncio + async def test_todo_mark_all_as_done(self, gl): + request = respx.post( + "http://localhost/api/v4/todos/mark_as_done", + headers={"content-type": "application/json"}, + content={}, + ) + + await gl.todos.mark_all_as_done() + + @respx.mock + @pytest.mark.asyncio + async def test_deployment(self, gl): + + content = '{"id": 42, "status": "success", "ref": "master"}' + json_content = json.loads(content) + + request_deployment_create = respx.post( + "http://localhost/api/v4/projects/1/deployments", + headers={"content-type": "application/json"}, + content=json_content, + status_code=StatusCode.OK, + ) + + project = await gl.projects.get(1, lazy=True) + deployment = await project.deployments.create( + { + "environment": "Test", + "sha": "1agf4gs", + "ref": "master", + "tag": False, + "status": "created", + } + ) + assert deployment.id == 42 + assert deployment.status == "success" + assert deployment.ref == "master" + + json_content["status"] = "failed" + request_deployment_update = respx.put( + "http://localhost/api/v4/projects/1/deployments/42", + headers={"content-type": "application/json"}, + content=json_content, + status_code=StatusCode.OK, + ) + deployment.status = "failed" + await deployment.save() + assert deployment.status == "failed" + + @respx.mock + @pytest.mark.asyncio + async def test_user_activate_deactivate(self, gl): + request_activate = respx.post( + "http://localhost/api/v4/users/1/activate", + headers={"content-type": "application/json"}, + content={}, + status_code=StatusCode.CREATED, + ) + request_deactivate = respx.post( + "http://localhost/api/v4/users/1/deactivate", + headers={"content-type": "application/json"}, + content={}, + status_code=StatusCode.CREATED, + ) + + user = await gl.users.get(1, lazy=True) + await user.activate() + await user.deactivate() + + @respx.mock + @pytest.mark.asyncio + async def test_update_submodule(self, gl): + request_get_project = respx.get( + "http://localhost/api/v4/projects/1", + headers={"content-type": "application/json"}, + content='{"name": "name", "id": 1}'.encode("utf-8"), + status_code=StatusCode.OK, + ) + request_update_submodule = respx.put( + "http://localhost/api/v4/projects/1/repository/submodules/foo%2Fbar", + headers={"content-type": "application/json"}, + content="""{ + "id": "ed899a2f4b50b4370feeea94676502b42383c746", + "short_id": "ed899a2f4b5", + "title": "Message", + "author_name": "Author", + "author_email": "author@example.com", + "committer_name": "Author", + "committer_email": "author@example.com", + "created_at": "2018-09-20T09:26:24.000-07:00", + "message": "Message", + "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], + "committed_date": "2018-09-20T09:26:24.000-07:00", + "authored_date": "2018-09-20T09:26:24.000-07:00", + "status": null}""".encode( + "utf-8" + ), + status_code=StatusCode.OK, + ) + project = await gl.projects.get(1) + assert isinstance(project, Project) + assert project.name == "name" + assert project.id == 1 + + ret = await project.update_submodule( + submodule="foo/bar", + branch="master", + commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664", + commit_message="Message", + ) + assert isinstance(ret, dict) + assert ret["message"] == "Message" + assert ret["id"] == "ed899a2f4b50b4370feeea94676502b42383c746" + + @respx.mock + @pytest.mark.asyncio + async def test_import_github(self, gl): + request = respx.post( + re.compile(r"^http://localhost/api/v4/import/github"), + headers={"content-type": "application/json"}, + content="""{ + "id": 27, + "name": "my-repo", + "full_path": "/root/my-repo", + "full_name": "Administrator / my-repo" + }""".encode( + "utf-8" + ), + status_code=StatusCode.OK, + ) + base_path = "/root" + name = "my-repo" + ret = await gl.projects.import_github("githubkey", 1234, base_path, name) + assert isinstance(ret, dict) + assert ret["name"] == name + assert ret["full_path"] == "/".join((base_path, name)) + assert ret["full_name"].endswith(name) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index b0f509da0..49fddec3c 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -172,402 +172,6 @@ def test_pickability(self): self.assertTrue(hasattr(unpickled, "_objects")) self.assertEqual(unpickled._objects, original_gl_objects) - def test_token_auth(self, callback=None): - name = "username" - id_ = 1 - - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/user", method="get") - def resp_cont(url, request): - headers = {"content-type": "application/json"} - content = '{{"id": {0:d}, "username": "{1:s}"}}'.format(id_, name).encode( - "utf-8" - ) - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - self.gl.auth() - self.assertEqual(self.gl.user.username, name) - self.assertEqual(self.gl.user.id, id_) - self.assertIsInstance(self.gl.user, CurrentUser) - - def test_hooks(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/hooks/1", method="get" - ) - def resp_get_hook(url, request): - headers = {"content-type": "application/json"} - content = '{"url": "testurl", "id": 1}'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_hook): - data = self.gl.hooks.get(1) - self.assertIsInstance(data, Hook) - self.assertEqual(data.url, "testurl") - self.assertEqual(data.id, 1) - - def test_projects(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects/1", method="get" - ) - def resp_get_project(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "name", "id": 1}'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_project): - data = self.gl.projects.get(1) - self.assertIsInstance(data, Project) - self.assertEqual(data.name, "name") - self.assertEqual(data.id, 1) - - def test_project_environments(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get" - ) - def resp_get_project(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "name", "id": 1}'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/environments/1", - method="get", - ) - def resp_get_environment(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "environment_name", "id": 1, "last_deployment": "sometime"}'.encode( - "utf-8" - ) - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_project, resp_get_environment): - project = self.gl.projects.get(1) - environment = project.environments.get(1) - self.assertIsInstance(environment, ProjectEnvironment) - self.assertEqual(environment.id, 1) - self.assertEqual(environment.last_deployment, "sometime") - self.assertEqual(environment.name, "environment_name") - - def test_project_additional_statistics(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get" - ) - def resp_get_project(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "name", "id": 1}'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/statistics", - method="get", - ) - def resp_get_environment(url, request): - headers = {"content-type": "application/json"} - content = """{"fetches": {"total": 50, "days": [{"count": 10, "date": "2018-01-10"}]}}""".encode( - "utf-8" - ) - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_project, resp_get_environment): - project = self.gl.projects.get(1) - statistics = project.additionalstatistics.get() - self.assertIsInstance(statistics, ProjectAdditionalStatistics) - self.assertEqual(statistics.fetches["total"], 50) - - def test_project_issues_statistics(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get" - ) - def resp_get_project(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "name", "id": 1}'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/issues_statistics", - method="get", - ) - def resp_get_environment(url, request): - headers = {"content-type": "application/json"} - content = """{"statistics": {"counts": {"all": 20, "closed": 5, "opened": 15}}}""".encode( - "utf-8" - ) - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_project, resp_get_environment): - project = self.gl.projects.get(1) - statistics = project.issuesstatistics.get() - self.assertIsInstance(statistics, ProjectIssuesStatistics) - self.assertEqual(statistics.statistics["counts"]["all"], 20) - - def test_groups(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/groups/1", method="get" - ) - def resp_get_group(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "name", "id": 1, "path": "path"}' - content = content.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_group): - data = self.gl.groups.get(1) - self.assertIsInstance(data, Group) - self.assertEqual(data.name, "name") - self.assertEqual(data.path, "path") - self.assertEqual(data.id, 1) - - def test_issues(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/issues", method="get" - ) - def resp_get_issue(url, request): - headers = {"content-type": "application/json"} - content = '[{"name": "name", "id": 1}, ' '{"name": "other_name", "id": 2}]' - content = content.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_issue): - data = self.gl.issues.list() - self.assertEqual(data[1].id, 2) - self.assertEqual(data[1].name, "other_name") - - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="get") - def resp_get_user(self, url, request): - headers = {"content-type": "application/json"} - content = ( - '{"name": "name", "id": 1, "password": "password", ' - '"username": "username", "email": "email"}' - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 5, request) - - def test_users(self): - with HTTMock(self.resp_get_user): - user = self.gl.users.get(1) - self.assertIsInstance(user, User) - self.assertEqual(user.name, "name") - self.assertEqual(user.id, 1) - - def test_user_status(self): - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/users/1/status", - method="get", - ) - def resp_get_user_status(url, request): - headers = {"content-type": "application/json"} - content = '{"message": "test", "message_html": "

Message

", "emoji": "thumbsup"}' - content = content.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(self.resp_get_user): - user = self.gl.users.get(1) - with HTTMock(resp_get_user_status): - status = user.status.get() - self.assertIsInstance(status, UserStatus) - self.assertEqual(status.message, "test") - self.assertEqual(status.emoji, "thumbsup") - - def test_todo(self): - with open(os.path.dirname(__file__) + "/data/todo.json", "r") as json_file: - todo_content = json_file.read() - json_content = json.loads(todo_content) - encoded_content = todo_content.encode("utf-8") - - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/todos", method="get") - def resp_get_todo(url, request): - headers = {"content-type": "application/json"} - return response(200, encoded_content, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/todos/102/mark_as_done", - method="post", - ) - def resp_mark_as_done(url, request): - headers = {"content-type": "application/json"} - single_todo = json.dumps(json_content[0]) - content = single_todo.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_todo): - todo = self.gl.todos.list()[0] - self.assertIsInstance(todo, Todo) - self.assertEqual(todo.id, 102) - self.assertEqual(todo.target_type, "MergeRequest") - self.assertEqual(todo.target["assignee"]["username"], "root") - with HTTMock(resp_mark_as_done): - todo.mark_as_done() - - def test_todo_mark_all_as_done(self): - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/todos/mark_as_done", - method="post", - ) - def resp_mark_all_as_done(url, request): - headers = {"content-type": "application/json"} - return response(204, {}, headers, None, 5, request) - - with HTTMock(resp_mark_all_as_done): - self.gl.todos.mark_all_as_done() - - def test_deployment(self): - content = '{"id": 42, "status": "success", "ref": "master"}' - json_content = json.loads(content) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/deployments", - method="post", - ) - def resp_deployment_create(url, request): - headers = {"content-type": "application/json"} - return response(200, json_content, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/deployments/42", - method="put", - ) - def resp_deployment_update(url, request): - headers = {"content-type": "application/json"} - return response(200, json_content, headers, None, 5, request) - - with HTTMock(resp_deployment_create): - deployment = self.gl.projects.get(1, lazy=True).deployments.create( - { - "environment": "Test", - "sha": "1agf4gs", - "ref": "master", - "tag": False, - "status": "created", - } - ) - self.assertEqual(deployment.id, 42) - self.assertEqual(deployment.status, "success") - self.assertEqual(deployment.ref, "master") - - with HTTMock(resp_deployment_update): - json_content["status"] = "failed" - deployment.status = "failed" - deployment.save() - self.assertEqual(deployment.status, "failed") - - def test_user_activate_deactivate(self): - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/users/1/activate", - method="post", - ) - def resp_activate(url, request): - headers = {"content-type": "application/json"} - return response(201, {}, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/users/1/deactivate", - method="post", - ) - def resp_deactivate(url, request): - headers = {"content-type": "application/json"} - return response(201, {}, headers, None, 5, request) - - with HTTMock(resp_activate), HTTMock(resp_deactivate): - self.gl.users.get(1, lazy=True).activate() - self.gl.users.get(1, lazy=True).deactivate() - - def test_update_submodule(self): - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get" - ) - def resp_get_project(url, request): - headers = {"content-type": "application/json"} - content = '{"name": "name", "id": 1}'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/repository/submodules/foo%2Fbar", - method="put", - ) - def resp_update_submodule(url, request): - headers = {"content-type": "application/json"} - content = """{ - "id": "ed899a2f4b50b4370feeea94676502b42383c746", - "short_id": "ed899a2f4b5", - "title": "Message", - "author_name": "Author", - "author_email": "author@example.com", - "committer_name": "Author", - "committer_email": "author@example.com", - "created_at": "2018-09-20T09:26:24.000-07:00", - "message": "Message", - "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], - "committed_date": "2018-09-20T09:26:24.000-07:00", - "authored_date": "2018-09-20T09:26:24.000-07:00", - "status": null}""" - content = content.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_get_project): - project = self.gl.projects.get(1) - self.assertIsInstance(project, Project) - self.assertEqual(project.name, "name") - self.assertEqual(project.id, 1) - with HTTMock(resp_update_submodule): - ret = project.update_submodule( - submodule="foo/bar", - branch="master", - commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664", - commit_message="Message", - ) - self.assertIsInstance(ret, dict) - self.assertEqual(ret["message"], "Message") - self.assertEqual(ret["id"], "ed899a2f4b50b4370feeea94676502b42383c746") - - def test_import_github(self): - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/import/github", - method="post", - ) - def resp_import_github(url, request): - headers = {"content-type": "application/json"} - content = """{ - "id": 27, - "name": "my-repo", - "full_path": "/root/my-repo", - "full_name": "Administrator / my-repo" - }""" - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - with HTTMock(resp_import_github): - base_path = "/root" - name = "my-repo" - ret = self.gl.projects.import_github("githubkey", 1234, base_path, name) - self.assertIsInstance(ret, dict) - self.assertEqual(ret["name"], name) - self.assertEqual(ret["full_path"], "/".join((base_path, name))) - self.assertTrue(ret["full_name"].endswith(name)) - def _default_config(self): fd, temp_path = tempfile.mkstemp() os.write(fd, valid_config) From 882cc2c4f087eb6488329e458e352e0f91a4c8a0 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Wed, 19 Feb 2020 14:38:59 +0300 Subject: [PATCH 07/15] fix: remove pickability for GitlabClient --- gitlab/tests/test_gitlab.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 49fddec3c..c7e337fac 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -164,14 +164,6 @@ def setUp(self): api_version=4, ) - def test_pickability(self): - original_gl_objects = self.gl._objects - pickled = pickle.dumps(self.gl) - unpickled = pickle.loads(pickled) - self.assertIsInstance(unpickled, Gitlab) - self.assertTrue(hasattr(unpickled, "_objects")) - self.assertEqual(unpickled._objects, original_gl_objects) - def _default_config(self): fd, temp_path = tempfile.mkstemp() os.write(fd, valid_config) From f694b203f07137bbcaa85b3a90e4e13a6bc75ca6 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Wed, 19 Feb 2020 19:07:00 +0300 Subject: [PATCH 08/15] test: transfer all test_mixins to async --- gitlab/base.py | 10 +- gitlab/tests/test_async_mixins.py | 266 ++++++++++++++++++++++++++++++ gitlab/tests/test_mixins.py | 239 --------------------------- 3 files changed, 271 insertions(+), 244 deletions(-) create mode 100644 gitlab/tests/test_async_mixins.py diff --git a/gitlab/base.py b/gitlab/base.py index a791db299..1291f7901 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -174,17 +174,17 @@ def __init__(self, manager, obj_cls, _list): self._obj_cls = obj_cls self._list = _list - def __iter__(self): + def __aiter__(self): return self def __len__(self): return len(self._list) - def __next__(self): - return self.next() + async def __anext__(self): + return await self.next() - def next(self): - data = self._list.next() + async def next(self): + data = await self._list.next() return self._obj_cls(self.manager, data) @property diff --git a/gitlab/tests/test_async_mixins.py b/gitlab/tests/test_async_mixins.py new file mode 100644 index 000000000..ecf329e19 --- /dev/null +++ b/gitlab/tests/test_async_mixins.py @@ -0,0 +1,266 @@ +import pytest +import respx +from httpx.status_codes import StatusCode + +from gitlab import Gitlab +from gitlab.base import RESTObject, RESTObjectList +from gitlab.mixins import ( + CreateMixin, + DeleteMixin, + GetMixin, + GetWithoutIdMixin, + ListMixin, + RefreshMixin, + SaveMixin, + SetMixin, + UpdateMixin, +) + +from .test_mixins import FakeManager, FakeObject + + +class TestMixinMethods: + @pytest.fixture + def gl(self): + return Gitlab("http://localhost", private_token="private_token", api_version=4) + + @respx.mock + @pytest.mark.asyncio + async def test_get_mixin(self, gl): + class M(GetMixin, FakeManager): + pass + + request = respx.get( + "http://localhost/api/v4/tests/42", + headers={"Content-Type": "application/json"}, + content={"id": 42, "foo": "bar"}, + status_code=StatusCode.OK, + ) + mgr = M(gl) + obj = await mgr.get(42) + assert isinstance(obj, FakeObject) + assert obj.foo == "bar" + assert obj.id == 42 + + @respx.mock + @pytest.mark.asyncio + async def test_refresh_mixin(self, gl): + class O(RefreshMixin, FakeObject): + pass + + request = respx.get( + "http://localhost/api/v4/tests/42", + headers={"Content-Type": "application/json"}, + content={"id": 42, "foo": "bar"}, + status_code=StatusCode.OK, + ) + mgr = FakeManager(gl) + obj = O(mgr, {"id": 42}) + res = await obj.refresh() + assert res is None + assert obj.foo == "bar" + assert obj.id == 42 + + @respx.mock + @pytest.mark.asyncio + async def test_get_without_id_mixin(self, gl): + class M(GetWithoutIdMixin, FakeManager): + pass + + request = respx.get( + "http://localhost/api/v4/tests", + headers={"Content-Type": "application/json"}, + content='{"foo": "bar"}', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + obj = await mgr.get() + assert isinstance(obj, FakeObject) + assert obj.foo == "bar" + assert not hasattr(obj, "id") + + @respx.mock + @pytest.mark.asyncio + async def test_list_mixin(self, gl): + class M(ListMixin, FakeManager): + pass + + request = respx.get( + "http://localhost/api/v4/tests", + headers={"Content-Type": "application/json"}, + content='[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + obj_list = await mgr.list(as_list=False) + assert isinstance(obj_list, RESTObjectList) + async for obj in obj_list: + assert isinstance(obj, FakeObject) + assert obj.id in (42, 43) + + obj_list = await mgr.list(all=True) + assert isinstance(obj_list, list) + assert obj_list[0].id == 42 + assert obj_list[1].id == 43 + assert isinstance(obj_list[0], FakeObject) + assert len(obj_list) == 2 + + @respx.mock + @pytest.mark.asyncio + async def test_list_other_url(self, gl): + class M(ListMixin, FakeManager): + pass + + request = respx.get( + "http://localhost/api/v4/others", + headers={"Content-Type": "application/json"}, + content='[{"id": 42, "foo": "bar"}]', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + obj_list = await mgr.list(path="/others", as_list=False) + assert isinstance(obj_list, RESTObjectList) + obj = await obj_list.next() + assert obj.id == 42 + assert obj.foo == "bar" + with pytest.raises(StopAsyncIteration): + await obj_list.next() + + @respx.mock + @pytest.mark.asyncio + async def test_create_mixin(self, gl): + class M(CreateMixin, FakeManager): + _create_attrs = (("foo",), ("bar", "baz")) + _update_attrs = (("foo",), ("bam",)) + + reqeust = respx.post( + "http://localhost/api/v4/tests", + headers={"Content-Type": "application/json"}, + content='{"id": 42, "foo": "bar"}', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + obj = await mgr.create({"foo": "bar"}) + assert isinstance(obj, FakeObject) + assert obj.id == 42 + assert obj.foo == "bar" + + @respx.mock + @pytest.mark.asyncio + async def test_create_mixin_custom_path(self, gl): + class M(CreateMixin, FakeManager): + _create_attrs = (("foo",), ("bar", "baz")) + _update_attrs = (("foo",), ("bam",)) + + request = respx.post( + "http://localhost/api/v4/others", + headers={"Content-Type": "application/json"}, + content='{"id": 42, "foo": "bar"}', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + obj = await mgr.create({"foo": "bar"}, path="/others") + assert isinstance(obj, FakeObject) + assert obj.id == 42 + assert obj.foo == "bar" + + @respx.mock + @pytest.mark.asyncio + async def test_update_mixin(self, gl): + class M(UpdateMixin, FakeManager): + _create_attrs = (("foo",), ("bar", "baz")) + _update_attrs = (("foo",), ("bam",)) + + request = respx.put( + "http://localhost/api/v4/tests/42", + headers={"Content-Type": "application/json"}, + content='{"id": 42, "foo": "baz"}', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + server_data = await mgr.update(42, {"foo": "baz"}) + assert isinstance(server_data, dict) + assert server_data["id"] == 42 + assert server_data["foo"] == "baz" + + @respx.mock + @pytest.mark.asyncio + async def test_update_mixin_no_id(self, gl): + class M(UpdateMixin, FakeManager): + _create_attrs = (("foo",), ("bar", "baz")) + _update_attrs = (("foo",), ("bam",)) + + request = respx.put( + "http://localhost/api/v4/tests", + headers={"Content-Type": "application/json"}, + content='{"foo": "baz"}', + status_code=StatusCode.OK, + ) + mgr = M(gl) + server_data = await mgr.update(new_data={"foo": "baz"}) + assert isinstance(server_data, dict) + assert server_data["foo"] == "baz" + + @respx.mock + @pytest.mark.asyncio + async def test_delete_mixin(self, gl): + class M(DeleteMixin, FakeManager): + pass + + request = respx.delete( + "http://localhost/api/v4/tests/42", + headers={"Content-Type": "application/json"}, + content="", + status_code=StatusCode.OK, + ) + + mgr = M(gl) + await mgr.delete(42) + + @respx.mock + @pytest.mark.asyncio + async def test_save_mixin(self, gl): + class M(UpdateMixin, FakeManager): + pass + + class O(SaveMixin, RESTObject): + pass + + request = respx.put( + "http://localhost/api/v4/tests/42", + headers={"Content-Type": "application/json"}, + content='{"id": 42, "foo": "baz"}', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + obj = O(mgr, {"id": 42, "foo": "bar"}) + obj.foo = "baz" + await obj.save() + assert obj._attrs["foo"] == "baz" + assert obj._updated_attrs == {} + + @respx.mock + @pytest.mark.asyncio + async def test_set_mixin(self, gl): + class M(SetMixin, FakeManager): + pass + + request = respx.put( + "http://localhost/api/v4/tests/foo", + headers={"Content-Type": "application/json"}, + content='{"key": "foo", "value": "bar"}', + status_code=StatusCode.OK, + ) + + mgr = M(gl) + obj = await mgr.set("foo", "bar") + assert isinstance(obj, FakeObject) + assert obj.key == "foo" + assert obj.value == "bar" diff --git a/gitlab/tests/test_mixins.py b/gitlab/tests/test_mixins.py index 749c0d260..6ecdb3800 100644 --- a/gitlab/tests/test_mixins.py +++ b/gitlab/tests/test_mixins.py @@ -138,110 +138,6 @@ def setUp(self): "http://localhost", private_token="private_token", api_version=4 ) - def test_get_mixin(self): - class M(GetMixin, FakeManager): - pass - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/tests/42", method="get" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"id": 42, "foo": "bar"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - obj = mgr.get(42) - self.assertIsInstance(obj, FakeObject) - self.assertEqual(obj.foo, "bar") - self.assertEqual(obj.id, 42) - - def test_refresh_mixin(self): - class O(RefreshMixin, FakeObject): - pass - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/tests/42", method="get" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"id": 42, "foo": "bar"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = FakeManager(self.gl) - obj = O(mgr, {"id": 42}) - res = obj.refresh() - self.assertIsNone(res) - self.assertEqual(obj.foo, "bar") - self.assertEqual(obj.id, 42) - - def test_get_without_id_mixin(self): - class M(GetWithoutIdMixin, FakeManager): - pass - - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"foo": "bar"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - obj = mgr.get() - self.assertIsInstance(obj, FakeObject) - self.assertEqual(obj.foo, "bar") - self.assertFalse(hasattr(obj, "id")) - - def test_list_mixin(self): - class M(ListMixin, FakeManager): - pass - - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - # test RESTObjectList - mgr = M(self.gl) - obj_list = mgr.list(as_list=False) - self.assertIsInstance(obj_list, base.RESTObjectList) - for obj in obj_list: - self.assertIsInstance(obj, FakeObject) - self.assertIn(obj.id, (42, 43)) - - # test list() - obj_list = mgr.list(all=True) - self.assertIsInstance(obj_list, list) - self.assertEqual(obj_list[0].id, 42) - self.assertEqual(obj_list[1].id, 43) - self.assertIsInstance(obj_list[0], FakeObject) - self.assertEqual(len(obj_list), 2) - - def test_list_other_url(self): - class M(ListMixin, FakeManager): - pass - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/others", method="get" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '[{"id": 42, "foo": "bar"}]' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - obj_list = mgr.list(path="/others", as_list=False) - self.assertIsInstance(obj_list, base.RESTObjectList) - obj = obj_list.next() - self.assertEqual(obj.id, 42) - self.assertEqual(obj.foo, "bar") - self.assertRaises(StopIteration, obj_list.next) - def test_create_mixin_get_attrs(self): class M1(CreateMixin, FakeManager): pass @@ -275,46 +171,6 @@ class M(CreateMixin, FakeManager): mgr._check_missing_create_attrs(data) self.assertIn("foo", str(error.exception)) - def test_create_mixin(self): - class M(CreateMixin, FakeManager): - _create_attrs = (("foo",), ("bar", "baz")) - _update_attrs = (("foo",), ("bam",)) - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/tests", method="post" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"id": 42, "foo": "bar"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - obj = mgr.create({"foo": "bar"}) - self.assertIsInstance(obj, FakeObject) - self.assertEqual(obj.id, 42) - self.assertEqual(obj.foo, "bar") - - def test_create_mixin_custom_path(self): - class M(CreateMixin, FakeManager): - _create_attrs = (("foo",), ("bar", "baz")) - _update_attrs = (("foo",), ("bam",)) - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/others", method="post" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"id": 42, "foo": "bar"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - obj = mgr.create({"foo": "bar"}, path="/others") - self.assertIsInstance(obj, FakeObject) - self.assertEqual(obj.id, 42) - self.assertEqual(obj.foo, "bar") - def test_update_mixin_get_attrs(self): class M1(UpdateMixin, FakeManager): pass @@ -347,98 +203,3 @@ class M(UpdateMixin, FakeManager): with self.assertRaises(AttributeError) as error: mgr._check_missing_update_attrs(data) self.assertIn("foo", str(error.exception)) - - def test_update_mixin(self): - class M(UpdateMixin, FakeManager): - _create_attrs = (("foo",), ("bar", "baz")) - _update_attrs = (("foo",), ("bam",)) - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/tests/42", method="put" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"id": 42, "foo": "baz"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - server_data = mgr.update(42, {"foo": "baz"}) - self.assertIsInstance(server_data, dict) - self.assertEqual(server_data["id"], 42) - self.assertEqual(server_data["foo"], "baz") - - def test_update_mixin_no_id(self): - class M(UpdateMixin, FakeManager): - _create_attrs = (("foo",), ("bar", "baz")) - _update_attrs = (("foo",), ("bam",)) - - @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="put") - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"foo": "baz"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - server_data = mgr.update(new_data={"foo": "baz"}) - self.assertIsInstance(server_data, dict) - self.assertEqual(server_data["foo"], "baz") - - def test_delete_mixin(self): - class M(DeleteMixin, FakeManager): - pass - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/tests/42", method="delete" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = "" - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - mgr.delete(42) - - def test_save_mixin(self): - class M(UpdateMixin, FakeManager): - pass - - class O(SaveMixin, RESTObject): - pass - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/tests/42", method="put" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"id": 42, "foo": "baz"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - obj = O(mgr, {"id": 42, "foo": "bar"}) - obj.foo = "baz" - obj.save() - self.assertEqual(obj._attrs["foo"], "baz") - self.assertDictEqual(obj._updated_attrs, {}) - - def test_set_mixin(self): - class M(SetMixin, FakeManager): - pass - - @urlmatch( - scheme="http", netloc="localhost", path="/api/v4/tests/foo", method="put" - ) - def resp_cont(url, request): - headers = {"Content-Type": "application/json"} - content = '{"key": "foo", "value": "bar"}' - return response(200, content, headers, None, 5, request) - - with HTTMock(resp_cont): - mgr = M(self.gl) - obj = mgr.set("foo", "bar") - self.assertIsInstance(obj, FakeObject) - self.assertEqual(obj.key, "foo") - self.assertEqual(obj.value, "bar") From a8ef796640eafe9ec3aa86803c77ff09116abaf0 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Thu, 20 Feb 2020 08:46:26 +0300 Subject: [PATCH 09/15] test: rework application tests in async way --- gitlab/tests/objects/test_application.py | 120 ------------------ .../tests/objects/test_async_application.py | 96 ++++++++++++++ 2 files changed, 96 insertions(+), 120 deletions(-) delete mode 100644 gitlab/tests/objects/test_application.py create mode 100644 gitlab/tests/objects/test_async_application.py diff --git a/gitlab/tests/objects/test_application.py b/gitlab/tests/objects/test_application.py deleted file mode 100644 index 50ca1ad50..000000000 --- a/gitlab/tests/objects/test_application.py +++ /dev/null @@ -1,120 +0,0 @@ -import unittest -import gitlab -import os -import pickle -import tempfile -import json -import unittest -import requests -from gitlab import * # noqa -from gitlab.v4.objects import * # noqa -from httmock import HTTMock, urlmatch, response # noqa - - -headers = {"content-type": "application/json"} - - -class TestApplicationAppearance(unittest.TestCase): - def setUp(self): - self.gl = Gitlab( - "http://localhost", - private_token="private_token", - ssl_verify=True, - api_version="4", - ) - self.title = "GitLab Test Instance" - self.new_title = "new-title" - self.description = "gitlab-test.example.com" - self.new_description = "new-description" - - def test_get_update_appearance(self): - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/application/appearance", - method="get", - ) - def resp_get_appearance(url, request): - content = """{ - "title": "%s", - "description": "%s", - "logo": "/uploads/-/system/appearance/logo/1/logo.png", - "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", - "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", - "new_project_guidelines": "Please read the FAQs for help.", - "header_message": "", - "footer_message": "", - "message_background_color": "#e75e40", - "message_font_color": "#ffffff", - "email_header_and_footer_enabled": false}""" % ( - self.title, - self.description, - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/application/appearance", - method="put", - ) - def resp_update_appearance(url, request): - content = """{ - "title": "%s", - "description": "%s", - "logo": "/uploads/-/system/appearance/logo/1/logo.png", - "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", - "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", - "new_project_guidelines": "Please read the FAQs for help.", - "header_message": "", - "footer_message": "", - "message_background_color": "#e75e40", - "message_font_color": "#ffffff", - "email_header_and_footer_enabled": false}""" % ( - self.new_title, - self.new_description, - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - with HTTMock(resp_get_appearance), HTTMock(resp_update_appearance): - appearance = self.gl.appearance.get() - self.assertEqual(appearance.title, self.title) - self.assertEqual(appearance.description, self.description) - appearance.title = self.new_title - appearance.description = self.new_description - appearance.save() - self.assertEqual(appearance.title, self.new_title) - self.assertEqual(appearance.description, self.new_description) - - def test_update_appearance(self): - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/application/appearance", - method="put", - ) - def resp_update_appearance(url, request): - content = """{ - "title": "%s", - "description": "%s", - "logo": "/uploads/-/system/appearance/logo/1/logo.png", - "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", - "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", - "new_project_guidelines": "Please read the FAQs for help.", - "header_message": "", - "footer_message": "", - "message_background_color": "#e75e40", - "message_font_color": "#ffffff", - "email_header_and_footer_enabled": false}""" % ( - self.new_title, - self.new_description, - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - with HTTMock(resp_update_appearance): - resp = self.gl.appearance.update( - title=self.new_title, description=self.new_description - ) diff --git a/gitlab/tests/objects/test_async_application.py b/gitlab/tests/objects/test_async_application.py new file mode 100644 index 000000000..6c5d34505 --- /dev/null +++ b/gitlab/tests/objects/test_async_application.py @@ -0,0 +1,96 @@ +import re + +import pytest +import respx +from httpx.status_codes import StatusCode + +from gitlab import Gitlab + + +class TestApplicationAppearance: + @pytest.fixture + def gl(self): + return Gitlab( + "http://localhost", + private_token="private_token", + ssl_verify=True, + api_version="4", + ) + + @respx.mock + @pytest.mark.asyncio + async def test_get_update_appearance(self, gl): + title = "GitLab Test Instance" + new_title = "new-title" + description = "gitlab-test.example.com" + new_description = "new-description" + + request_get_appearance = respx.get( + "http://localhost/api/v4/application/appearance", + content={ + "title": title, + "description": description, + "logo": "/uploads/-/system/appearance/logo/1/logo.png", + "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", + "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", + "new_project_guidelines": "Please read the FAQs for help.", + "header_message": "", + "footer_message": "", + "message_background_color": "#e75e40", + "message_font_color": "#ffffff", + "email_header_and_footer_enabled": False, + }, + status_code=StatusCode.OK, + ) + request_update_appearance = respx.put( + "http://localhost/api/v4/application/appearance", + content={ + "title": new_title, + "description": new_description, + "logo": "/uploads/-/system/appearance/logo/1/logo.png", + "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", + "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", + "new_project_guidelines": "Please read the FAQs for help.", + "header_message": "", + "footer_message": "", + "message_background_color": "#e75e40", + "message_font_color": "#ffffff", + "email_header_and_footer_enabled": False, + }, + status_code=StatusCode.OK, + ) + + appearance = await gl.appearance.get() + assert appearance.title == title + assert appearance.description == description + appearance.title = new_title + appearance.description = new_description + await appearance.save() + assert appearance.title == new_title + assert appearance.description == new_description + + @respx.mock + @pytest.mark.asyncio + async def test_update_appearance(self, gl): + new_title = "new-title" + new_description = "new-description" + + request = respx.put( + re.compile("^http://localhost/api/v4/application/appearance"), + content={ + "title": new_title, + "description": new_description, + "logo": "/uploads/-/system/appearance/logo/1/logo.png", + "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", + "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", + "new_project_guidelines": "Please read the FAQs for help.", + "header_message": "", + "footer_message": "", + "message_background_color": "#e75e40", + "message_font_color": "#ffffff", + "email_header_and_footer_enabled": False, + }, + status_code=StatusCode.OK, + ) + + await gl.appearance.update(title=new_title, description=new_description) From 4352f9bb487d0ed1c6414045bcf13e2681dbfb7b Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Thu, 20 Feb 2020 09:13:21 +0300 Subject: [PATCH 10/15] test: rework tests on projects snippets in async way --- gitlab/tests/objects/test_async_projects.py | 110 +++++++++++++++ gitlab/tests/objects/test_projects.py | 140 -------------------- 2 files changed, 110 insertions(+), 140 deletions(-) create mode 100644 gitlab/tests/objects/test_async_projects.py delete mode 100644 gitlab/tests/objects/test_projects.py diff --git a/gitlab/tests/objects/test_async_projects.py b/gitlab/tests/objects/test_async_projects.py new file mode 100644 index 000000000..59921998a --- /dev/null +++ b/gitlab/tests/objects/test_async_projects.py @@ -0,0 +1,110 @@ +import pytest +import respx +from httpx.status_codes import StatusCode + +from gitlab import Gitlab + + +class TestProjectSnippets: + @pytest.fixture + def gl(self): + return Gitlab( + "http://localhost", + private_token="private_token", + ssl_verify=True, + api_version=4, + ) + + @respx.mock + @pytest.mark.asyncio + async def test_list_project_snippets(self, gl): + title = "Example Snippet Title" + visibility = "private" + request = respx.get( + "http://localhost/api/v4/projects/1/snippets", + content=[ + { + "title": title, + "description": "More verbose snippet description", + "file_name": "example.txt", + "content": "source code with multiple lines", + "visibility": visibility, + } + ], + status_code=StatusCode.OK, + ) + + project = await gl.projects.get(1, lazy=True) + snippets = await project.snippets.list() + assert len(snippets) == 1 + assert snippets[0].title == title + assert snippets[0].visibility == visibility + + @respx.mock + @pytest.mark.asyncio + async def test_get_project_snippet(self, gl): + title = "Example Snippet Title" + visibility = "private" + request = respx.get( + "http://localhost/api/v4/projects/1/snippets/1", + content={ + "title": title, + "description": "More verbose snippet description", + "file_name": "example.txt", + "content": "source code with multiple lines", + "visibility": visibility, + }, + status_code=StatusCode.OK, + ) + + project = await gl.projects.get(1, lazy=True) + snippet = await project.snippets.get(1) + assert snippet.title == title + assert snippet.visibility == visibility + + @respx.mock + @pytest.mark.asyncio + async def test_create_update_project_snippets(self, gl): + title = "Example Snippet Title" + new_title = "new-title" + visibility = "private" + request_update = respx.put( + "http://localhost/api/v4/projects/1/snippets", + content={ + "title": new_title, + "description": "More verbose snippet description", + "file_name": "example.txt", + "content": "source code with multiple lines", + "visibility": visibility, + }, + status_code=StatusCode.OK, + ) + + request_create = respx.post( + "http://localhost/api/v4/projects/1/snippets", + content={ + "title": title, + "description": "More verbose snippet description", + "file_name": "example.txt", + "content": "source code with multiple lines", + "visibility": visibility, + }, + status_code=StatusCode.OK, + ) + + project = await gl.projects.get(1, lazy=True) + snippet = await project.snippets.create( + { + "title": title, + "file_name": title, + "content": title, + "visibility": visibility, + } + ) + assert snippet.title == title + assert snippet.visibility == visibility + + snippet.title = new_title + await snippet.save() + assert snippet.title == new_title + assert snippet.visibility == visibility diff --git a/gitlab/tests/objects/test_projects.py b/gitlab/tests/objects/test_projects.py deleted file mode 100644 index 237a9bee7..000000000 --- a/gitlab/tests/objects/test_projects.py +++ /dev/null @@ -1,140 +0,0 @@ -import unittest -import gitlab -import os -import pickle -import tempfile -import json -import unittest -import requests -from gitlab import * # noqa -from gitlab.v4.objects import * # noqa -from httmock import HTTMock, urlmatch, response # noqa - - -headers = {"content-type": "application/json"} - - -class TestProjectSnippets(unittest.TestCase): - def setUp(self): - self.gl = Gitlab( - "http://localhost", - private_token="private_token", - ssl_verify=True, - api_version=4, - ) - - def test_list_project_snippets(self): - title = "Example Snippet Title" - visibility = "private" - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/snippets", - method="get", - ) - def resp_list_snippet(url, request): - content = """[{ - "title": "%s", - "description": "More verbose snippet description", - "file_name": "example.txt", - "content": "source code with multiple lines", - "visibility": "%s"}]""" % ( - title, - visibility, - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - with HTTMock(resp_list_snippet): - snippets = self.gl.projects.get(1, lazy=True).snippets.list() - self.assertEqual(len(snippets), 1) - self.assertEqual(snippets[0].title, title) - self.assertEqual(snippets[0].visibility, visibility) - - def test_get_project_snippets(self): - title = "Example Snippet Title" - visibility = "private" - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/snippets/1", - method="get", - ) - def resp_get_snippet(url, request): - content = """{ - "title": "%s", - "description": "More verbose snippet description", - "file_name": "example.txt", - "content": "source code with multiple lines", - "visibility": "%s"}""" % ( - title, - visibility, - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - with HTTMock(resp_get_snippet): - snippet = self.gl.projects.get(1, lazy=True).snippets.get(1) - self.assertEqual(snippet.title, title) - self.assertEqual(snippet.visibility, visibility) - - def test_create_update_project_snippets(self): - title = "Example Snippet Title" - visibility = "private" - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/snippets", - method="put", - ) - def resp_update_snippet(url, request): - content = """{ - "title": "%s", - "description": "More verbose snippet description", - "file_name": "example.txt", - "content": "source code with multiple lines", - "visibility": "%s"}""" % ( - title, - visibility, - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - @urlmatch( - scheme="http", - netloc="localhost", - path="/api/v4/projects/1/snippets", - method="post", - ) - def resp_create_snippet(url, request): - content = """{ - "title": "%s", - "description": "More verbose snippet description", - "file_name": "example.txt", - "content": "source code with multiple lines", - "visibility": "%s"}""" % ( - title, - visibility, - ) - content = content.encode("utf-8") - return response(200, content, headers, None, 25, request) - - with HTTMock(resp_create_snippet, resp_update_snippet): - snippet = self.gl.projects.get(1, lazy=True).snippets.create( - { - "title": title, - "file_name": title, - "content": title, - "visibility": visibility, - } - ) - self.assertEqual(snippet.title, title) - self.assertEqual(snippet.visibility, visibility) - title = "new-title" - snippet.title = title - snippet.save() - self.assertEqual(snippet.title, title) - self.assertEqual(snippet.visibility, visibility) From 37dbc09f02b7954edae1f81c69e52706b67719b3 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Fri, 21 Feb 2020 09:13:22 +0300 Subject: [PATCH 11/15] fix: response content is not async --- gitlab/tests/test_gitlab.py | 1 - gitlab/utils.py | 4 ++-- gitlab/v4/objects.py | 24 ++++++++++++------------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index c7e337fac..14c0d0144 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -23,7 +23,6 @@ import unittest import httpx -import requests from httmock import HTTMock # noqa from httmock import response # noqa from httmock import urlmatch # noqa diff --git a/gitlab/utils.py b/gitlab/utils.py index 4241787a8..afe103d50 100644 --- a/gitlab/utils.py +++ b/gitlab/utils.py @@ -23,14 +23,14 @@ def __call__(self, chunk): print(chunk) -def response_content(response, streamed, action, chunk_size): +async def response_content(response, streamed, action): if streamed is False: return response.content if action is None: action = _StdoutStream() - for chunk in response.iter_content(chunk_size=chunk_size): + async for chunk in response.aiter_bytes(): if chunk: action(chunk) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index ad6101d5b..40570dff8 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1566,7 +1566,7 @@ async def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) class SnippetManager(CRUDMixin, RESTManager): @@ -1905,7 +1905,7 @@ async def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) @@ -1935,7 +1935,7 @@ async def artifact( result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) @@ -1962,7 +1962,7 @@ async def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) class ProjectJobManager(RetrieveMixin, RESTManager): @@ -3260,14 +3260,14 @@ async def delete(self, name, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ - self.gitlab.http_delete(self.path, query_data={"name": name}, **kwargs) + await self.gitlab.http_delete(self.path, query_data={"name": name}, **kwargs) class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "file_path" _short_print_attr = "file_path" - async def decode(self): + def decode(self): """Returns the decoded content of the file. Returns: @@ -3445,10 +3445,10 @@ async def raw( file_path = file_path.replace("/", "%2F").replace(".", "%2E") path = "%s/%s/raw" % (self.path, file_path) query_data = {"ref": ref} - result = self.gitlab.http_get( + result = await self.gitlab.http_get( path, query_data=query_data, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) @exc.on_http_error(exc.GitlabListError) @@ -3758,7 +3758,7 @@ async def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) class ProjectSnippetManager(CRUDMixin, RESTManager): @@ -4078,7 +4078,7 @@ async def download(self, streamed=False, action=None, chunk_size=1024, **kwargs) result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) class ProjectExportManager(GetWithoutIdMixin, CreateMixin, RESTManager): @@ -4348,7 +4348,7 @@ async def repository_archive( result = await self.manager.gitlab.http_get( path, query_data=query_data, raw=True, streamed=streamed, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) @cli.register_custom_action("Project", ("forked_from_id",)) @exc.on_http_error(exc.GitlabCreateError) @@ -4628,7 +4628,7 @@ async def snapshot( result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) @cli.register_custom_action("Project", ("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) From 1563b7d6fe4f447a4d3b62461ff6cdb283c2a354 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Fri, 21 Feb 2020 09:14:14 +0300 Subject: [PATCH 12/15] fix: add missing awaits in v4 objects --- gitlab/v4/objects.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 40570dff8..b5e5a0f1d 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2010,7 +2010,7 @@ async def create(self, data, **kwargs): path = base_path % data else: path = self._compute_path(base_path) - return CreateMixin.create(self, data, path=path, **kwargs) + return await CreateMixin.create(self, data, path=path, **kwargs) class ProjectCommitComment(RESTObject): @@ -2268,7 +2268,7 @@ async def create(self, data, **kwargs): the data sent by the server """ path = self.path[:-1] # drop the 's' - return CreateMixin.create(self, data, path=path, **kwargs) + return await CreateMixin.create(self, data, path=path, **kwargs) class ProjectHook(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -3292,7 +3292,7 @@ async def save(self, branch, commit_message, **kwargs): self.branch = branch self.commit_message = commit_message self.file_path = self.file_path.replace("/", "%2F") - super(ProjectFile, self).save(**kwargs) + await super(ProjectFile, self).save(**kwargs) async def delete(self, branch, commit_message, **kwargs): """Delete the file from the server. @@ -3307,7 +3307,7 @@ async def delete(self, branch, commit_message, **kwargs): GitlabDeleteError: If the server cannot perform the request """ file_path = self.get_id().replace("/", "%2F") - self.manager.delete(file_path, branch, commit_message, **kwargs) + await self.manager.delete(file_path, branch, commit_message, **kwargs) class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager): @@ -3340,7 +3340,7 @@ async def get(self, file_path, ref, **kwargs): object: The generated RESTObject """ file_path = file_path.replace("/", "%2F") - return GetMixin.get(self, file_path, ref=ref, **kwargs) + return await GetMixin.get(self, file_path, ref=ref, **kwargs) @cli.register_custom_action( "ProjectFileManager", @@ -3415,7 +3415,7 @@ async def delete(self, file_path, branch, commit_message, **kwargs): """ path = "%s/%s" % (self.path, file_path.replace("/", "%2F")) data = {"branch": branch, "commit_message": commit_message} - self.gitlab.http_delete(path, query_data=data, **kwargs) + await self.gitlab.http_delete(path, query_data=data, **kwargs) @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) @exc.on_http_error(exc.GitlabGetError) @@ -3565,7 +3565,7 @@ async def create(self, data, **kwargs): the data sent by the server """ path = self.path[:-1] # drop the 's' - return CreateMixin.create(self, data, path=path, **kwargs) + return await CreateMixin.create(self, data, path=path, **kwargs) class ProjectPipelineScheduleVariable(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -3896,7 +3896,7 @@ async def get(self, id, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ - obj = super(ProjectServiceManager, self).get(id, **kwargs) + obj = await super(ProjectServiceManager, self).get(id, **kwargs) obj.id = id return obj @@ -3916,11 +3916,11 @@ async def update(self, id=None, new_data=None, **kwargs): GitlabUpdateError: If the server cannot perform the request """ new_data = new_data or {} - super(ProjectServiceManager, self).update(id, new_data, **kwargs) + await super(ProjectServiceManager, self).update(id, new_data, **kwargs) self.id = id @cli.register_custom_action("ProjectServiceManager") - async def available(self, **kwargs): + def available(self, **kwargs): """List the services known by python-gitlab. Returns: @@ -3976,7 +3976,7 @@ async def set_approvers(self, approver_ids=None, approver_group_ids=None, **kwar path = "/projects/%s/approvers" % self._parent.get_id() data = {"approver_ids": approver_ids, "approver_group_ids": approver_group_ids} - self.gitlab.http_put(path, post_data=data, **kwargs) + await self.gitlab.http_put(path, post_data=data, **kwargs) class ProjectApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -4271,7 +4271,7 @@ async def repository_raw_blob( result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) @cli.register_custom_action("Project", ("from_", "to")) @exc.on_http_error(exc.GitlabGetError) @@ -4688,14 +4688,7 @@ async def transfer_project(self, to_namespace, **kwargs): @cli.register_custom_action("Project", ("ref_name", "artifact_path", "job")) @exc.on_http_error(exc.GitlabGetError) async def artifact( - self, - ref_name, - artifact_path, - job, - streamed=False, - action=None, - chunk_size=1024, - **kwargs + self, ref_name, artifact_path, job, streamed=False, action=None, **kwargs ): """Download a single artifact file from a specific tag or branch from within the job’s artifacts archive. @@ -4728,7 +4721,7 @@ async def artifact( result = await self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + return await utils.response_content(result, streamed, action) class ProjectManager(CRUDMixin, RESTManager): From f933cd002ff50eaebc67bc6e5ef03bf5d8cec2be Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Fri, 21 Feb 2020 09:14:49 +0300 Subject: [PATCH 13/15] fix: use `overwrite` as string in import project method This possibly is temporary solution because httpx raises TypeError when tries to init DataField with bool value --- gitlab/v4/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index b5e5a0f1d..d42888f3e 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -4830,7 +4830,7 @@ async def import_project( dict: A representation of the import status. """ files = {"file": ("file.tar.gz", file)} - data = {"path": path, "overwrite": overwrite} + data = {"path": path, "overwrite": "true" if overwrite else "false"} if override_params: for k, v in override_params.items(): data["override_params[%s]" % k] = v From efb1424d8e0f900ea6d69c00c3cca8f307967f34 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Fri, 21 Feb 2020 09:22:33 +0300 Subject: [PATCH 14/15] test: add integration tests in async way --- tools/python_async_test_v4.py | 1003 +++++++++++++++++++++++++++++++++ 1 file changed, 1003 insertions(+) create mode 100644 tools/python_async_test_v4.py diff --git a/tools/python_async_test_v4.py b/tools/python_async_test_v4.py new file mode 100644 index 000000000..021fa730b --- /dev/null +++ b/tools/python_async_test_v4.py @@ -0,0 +1,1003 @@ +import asyncio +import base64 +import os + +import httpx + +import gitlab + +LOGIN = "root" +PASSWORD = "5iveL!fe" + +SSH_KEY = ( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih" + "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n" + "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l" + "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI" + "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh" + "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar" +) +DEPLOY_KEY = ( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG" + "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI" + "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6" + "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu" + "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv" + "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc" + "vn bar@foo" +) + +GPG_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFn5mzYBCADH6SDVPAp1zh/hxmTi0QplkOfExBACpuY6OhzNdIg+8/528b3g +Y5YFR6T/HLv/PmeHskUj21end1C0PNG2T9dTx+2Vlh9ISsSG1kyF9T5fvMR3bE0x +Dl6S489CXZrjPTS9SHk1kF+7dwjUxLJyxF9hPiSihFefDFu3NeOtG/u8vbC1mewQ +ZyAYue+mqtqcCIFFoBz7wHKMWjIVSJSyTkXExu4OzpVvy3l2EikbvavI3qNz84b+ +Mgkv/kiBlNoCy3CVuPk99RYKZ3lX1vVtqQ0OgNGQvb4DjcpyjmbKyibuZwhDjIOh +au6d1OyEbayTntd+dQ4j9EMSnEvm/0MJ4eXPABEBAAG0G0dpdGxhYlRlc3QxIDxm +YWtlQGZha2UudGxkPokBNwQTAQgAIQUCWfmbNgIbAwULCQgHAgYVCAkKCwIEFgID +AQIeAQIXgAAKCRBgxELHf8f3hF3yB/wNJlWPKY65UsB4Lo0hs1OxdxCDqXogSi0u +6crDEIiyOte62pNZKzWy8TJcGZvznRTZ7t8hXgKFLz3PRMcl+vAiRC6quIDUj+2V +eYfwaItd1lUfzvdCaC7Venf4TQ74f5vvNg/zoGwE6eRoSbjlLv9nqsxeA0rUBUQL +LYikWhVMP3TrlfgfduYvh6mfgh57BDLJ9kJVpyfxxx9YLKZbaas9sPa6LgBtR555 +JziUxHmbEv8XCsUU8uoFeP1pImbNBplqE3wzJwzOMSmmch7iZzrAwfN7N2j3Wj0H +B5kQddJ9dmB4BbU0IXGhWczvdpxboI2wdY8a1JypxOdePoph/43iuQENBFn5mzYB +CADnTPY0Zf3d9zLjBNgIb3yDl94uOcKCq0twNmyjMhHzGqw+UMe9BScy34GL94Al +xFRQoaL+7P8hGsnsNku29A/VDZivcI+uxTx4WQ7OLcn7V0bnHV4d76iky2ufbUt/ +GofthjDs1SonePO2N09sS4V4uK0d5N4BfCzzXgvg8etCLxNmC9BGt7AaKUUzKBO4 +2QvNNaC2C/8XEnOgNWYvR36ylAXAmo0sGFXUsBCTiq1fugS9pwtaS2JmaVpZZ3YT +pMZlS0+SjC5BZYFqSmKCsA58oBRzCxQz57nR4h5VEflgD+Hy0HdW0UHETwz83E6/ +U0LL6YyvhwFr6KPq5GxinSvfABEBAAGJAR8EGAEIAAkFAln5mzYCGwwACgkQYMRC +x3/H94SJgwgAlKQb10/xcL/epdDkR7vbiei7huGLBpRDb/L5fM8B5W77Qi8Xmuqj +cCu1j99ZCA5hs/vwVn8j8iLSBGMC5gxcuaar/wtmiaEvT9fO/h6q4opG7NcuiJ8H +wRj8ccJmRssNqDD913PLz7T40Ts62blhrEAlJozGVG/q7T3RAZcskOUHKeHfc2RI +YzGsC/I9d7k6uxAv1L9Nm5F2HaAQDzhkdd16nKkGaPGR35cT1JLInkfl5cdm7ldN +nxs4TLO3kZjUTgWKdhpgRNF5hwaz51ZjpebaRf/ZqRuNyX4lIRolDxzOn/+O1o8L +qG2ZdhHHmSK2LaQLFiSprUkikStNU9BqSQ== +=5OGa +-----END PGP PUBLIC KEY BLOCK-----""" +AVATAR_PATH = os.path.join(os.path.dirname(__file__), "avatar.png") + + +async def main(): + # token authentication from config file + gl = gitlab.Gitlab.from_config(config_files=["/tmp/python-gitlab.cfg"]) + gl.enable_debug() + await gl.auth() + assert isinstance(gl.user, gitlab.v4.objects.CurrentUser) + + # markdown + html = await gl.markdown("foo") + assert "foo" in html + + success, errors = await gl.lint("Invalid") + assert success is False + assert errors + + # sidekiq + out = await gl.sidekiq.queue_metrics() + assert isinstance(out, dict) + assert "pages" in out["queues"] + out = await gl.sidekiq.process_metrics() + assert isinstance(out, dict) + assert "hostname" in out["processes"][0] + out = await gl.sidekiq.job_stats() + assert isinstance(out, dict) + assert "processed" in out["jobs"] + out = await gl.sidekiq.compound_metrics() + assert isinstance(out, dict) + assert "jobs" in out + assert "processes" in out + assert "queues" in out + + # settings + settings = await gl.settings.get() + settings.default_projects_limit = 42 + await settings.save() + settings = await gl.settings.get() + assert settings.default_projects_limit == 42 + + # users + new_user = await gl.users.create( + { + "email": "foo@bar.com", + "username": "foo", + "name": "foo", + "password": "foo_password", + "avatar": open(AVATAR_PATH, "rb"), + } + ) + avatar_url = new_user.avatar_url.replace("gitlab.test", "localhost:8080") + uploaded_avatar = httpx.get(avatar_url).content + assert uploaded_avatar == open(AVATAR_PATH, "rb").read() + users_list = await gl.users.list() + for user in users_list: + if user.username == "foo": + break + assert new_user.username == user.username + assert new_user.email == user.email + + await new_user.block() + await new_user.unblock() + + # user projects list + assert len(await new_user.projects.list()) == 0 + + # events list + await new_user.events.list() + + foobar_user = await gl.users.create( + { + "email": "foobar@example.com", + "username": "foobar", + "name": "Foo Bar", + "password": "foobar_password", + } + ) + + assert (await gl.users.list(search="foobar"))[0].id == foobar_user.id + expected = [new_user, foobar_user] + actual = list(await gl.users.list(search="foo")) + assert len(expected) == len(actual) + assert len(await gl.users.list(search="asdf")) == 0 + foobar_user.bio = "This is the user bio" + await foobar_user.save() + + # GPG keys + gkey = await new_user.gpgkeys.create({"key": GPG_KEY}) + assert len(await new_user.gpgkeys.list()) == 1 + # Seems broken on the gitlab side + # gkey = new_user.gpgkeys.get(gkey.id) + await gkey.delete() + assert len(await new_user.gpgkeys.list()) == 0 + + # SSH keys + key = await new_user.keys.create({"title": "testkey", "key": SSH_KEY}) + assert len(await new_user.keys.list()) == 1 + await key.delete() + assert len(await new_user.keys.list()) == 0 + + # emails + email = await new_user.emails.create({"email": "foo2@bar.com"}) + assert len(await new_user.emails.list()) == 1 + await email.delete() + assert len(await new_user.emails.list()) == 0 + + # custom attributes + attrs = await new_user.customattributes.list() + assert len(attrs) == 0 + attr = await new_user.customattributes.set("key", "value1") + assert len(await gl.users.list(custom_attributes={"key": "value1"})) == 1 + assert attr.key == "key" + assert attr.value == "value1" + assert len(await new_user.customattributes.list()) == 1 + attr = await new_user.customattributes.set("key", "value2") + attr = await new_user.customattributes.get("key") + assert attr.value == "value2" + assert len(await new_user.customattributes.list()) == 1 + await attr.delete() + assert len(await new_user.customattributes.list()) == 0 + + # impersonation tokens + user_token = await new_user.impersonationtokens.create( + {"name": "token1", "scopes": ["api", "read_user"]} + ) + l = await new_user.impersonationtokens.list(state="active") + assert len(l) == 1 + await user_token.delete() + l = await new_user.impersonationtokens.list(state="active") + assert len(l) == 0 + l = await new_user.impersonationtokens.list(state="inactive") + assert len(l) == 1 + + await new_user.delete() + await foobar_user.delete() + assert len(await gl.users.list()) == 3 + len( + [u for u in await gl.users.list() if u.username == "ghost"] + ) + + # current user mail + mail = await gl.user.emails.create({"email": "current@user.com"}) + assert len(await gl.user.emails.list()) == 1 + await mail.delete() + assert len(await gl.user.emails.list()) == 0 + + # current user GPG keys + gkey = await gl.user.gpgkeys.create({"key": GPG_KEY}) + assert len(await gl.user.gpgkeys.list()) == 1 + # Seems broken on the gitlab side + gkey = await gl.user.gpgkeys.get(gkey.id) + await gkey.delete() + assert len(await gl.user.gpgkeys.list()) == 0 + + # current user key + key = await gl.user.keys.create({"title": "testkey", "key": SSH_KEY}) + assert len(await gl.user.keys.list()) == 1 + await key.delete() + assert len(await gl.user.keys.list()) == 0 + + # templates + assert await gl.dockerfiles.list() + dockerfile = await gl.dockerfiles.get("Node") + assert dockerfile.content is not None + + assert await gl.gitignores.list() + gitignore = await gl.gitignores.get("Node") + assert gitignore.content is not None + + assert await gl.gitlabciymls.list() + gitlabciyml = await gl.gitlabciymls.get("Nodejs") + assert gitlabciyml.content is not None + + assert await gl.licenses.list() + license = await gl.licenses.get( + "bsd-2-clause", project="mytestproject", fullname="mytestfullname" + ) + assert "mytestfullname" in license.content + + # groups + user1 = await gl.users.create( + { + "email": "user1@test.com", + "username": "user1", + "name": "user1", + "password": "user1_pass", + } + ) + user2 = await gl.users.create( + { + "email": "user2@test.com", + "username": "user2", + "name": "user2", + "password": "user2_pass", + } + ) + group1 = await gl.groups.create({"name": "group1", "path": "group1"}) + group2 = await gl.groups.create({"name": "group2", "path": "group2"}) + + p_id = (await gl.groups.list(search="group2"))[0].id + group3 = await gl.groups.create( + {"name": "group3", "path": "group3", "parent_id": p_id} + ) + + assert len(await gl.groups.list()) == 3 + assert len(await gl.groups.list(search="oup1")) == 1 + assert group3.parent_id == p_id + assert (await group2.subgroups.list())[0].id == group3.id + + await group1.members.create( + {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user1.id} + ) + await group1.members.create( + {"access_level": gitlab.const.GUEST_ACCESS, "user_id": user2.id} + ) + + await group2.members.create( + {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user2.id} + ) + + # Administrator belongs to the groups + assert len(await group1.members.list()) == 3 + assert len(await group2.members.list()) == 2 + + await group1.members.delete(user1.id) + assert len(await group1.members.list()) == 2 + assert len(await group1.members.all()) + member = await group1.members.get(user2.id) + member.access_level = gitlab.const.OWNER_ACCESS + await member.save() + member = await group1.members.get(user2.id) + assert member.access_level == gitlab.const.OWNER_ACCESS + + await group2.members.delete(gl.user.id) + + # group custom attributes + attrs = await group2.customattributes.list() + assert len(attrs) == 0 + attr = await group2.customattributes.set("key", "value1") + assert len(await gl.groups.list(custom_attributes={"key": "value1"})) == 1 + assert attr.key == "key" + assert attr.value == "value1" + assert len(await group2.customattributes.list()) == 1 + attr = await group2.customattributes.set("key", "value2") + attr = await group2.customattributes.get("key") + assert attr.value == "value2" + assert len(await group2.customattributes.list()) == 1 + await attr.delete() + assert len(await group2.customattributes.list()) == 0 + + # group notification settings + settings = await group2.notificationsettings.get() + settings.level = "disabled" + await settings.save() + settings = await group2.notificationsettings.get() + assert settings.level == "disabled" + + # group badges + badge_image = "http://example.com" + badge_link = "http://example/img.svg" + badge = await group2.badges.create( + {"link_url": badge_link, "image_url": badge_image} + ) + assert len(await group2.badges.list()) == 1 + badge.image_url = "http://another.example.com" + await badge.save() + badge = await group2.badges.get(badge.id) + assert badge.image_url == "http://another.example.com" + await badge.delete() + assert len(await group2.badges.list()) == 0 + + # group milestones + gm1 = await group1.milestones.create({"title": "groupmilestone1"}) + assert len(await group1.milestones.list()) == 1 + gm1.due_date = "2020-01-01T00:00:00Z" + await gm1.save() + gm1.state_event = "close" + await gm1.save() + gm1 = await group1.milestones.get(gm1.id) + assert gm1.state == "closed" + assert len(await gm1.issues()) == 0 + assert len(await gm1.merge_requests()) == 0 + + # group variables + await group1.variables.create({"key": "foo", "value": "bar"}) + g_v = await group1.variables.get("foo") + assert g_v.value == "bar" + g_v.value = "baz" + await g_v.save() + g_v = await group1.variables.get("foo") + assert g_v.value == "baz" + assert len(await group1.variables.list()) == 1 + await g_v.delete() + assert len(await group1.variables.list()) == 0 + + # group labels + # group1.labels.create({"name": "foo", "description": "bar", "color": "#112233"}) + # g_l = group1.labels.get("foo") + # assert g_l.description == "bar" + # g_l.description = "baz" + # g_l.save() + # g_l = group1.labels.get("foo") + # assert g_l.description == "baz" + # assert len(group1.labels.list()) == 1 + # g_l.delete() + # assert len(group1.labels.list()) == 0 + + # hooks + hook = await gl.hooks.create({"url": "http://whatever.com"}) + assert len(await gl.hooks.list()) == 1 + await hook.delete() + assert len(await gl.hooks.list()) == 0 + + # projects + admin_project = await gl.projects.create({"name": "admin_project"}) + gr1_project = await gl.projects.create( + {"name": "gr1_project", "namespace_id": group1.id} + ) + gr2_project = await gl.projects.create( + {"name": "gr2_project", "namespace_id": group2.id} + ) + sudo_project = await gl.projects.create({"name": "sudo_project"}, sudo=user1.name) + + assert len(await gl.projects.list(owned=True)) == 2 + assert len(await gl.projects.list(search="admin")) == 1 + + # test pagination + l1 = await gl.projects.list(per_page=1, page=1) + l2 = await gl.projects.list(per_page=1, page=2) + assert len(l1) == 1 + assert len(l2) == 1 + assert l1[0].id != l2[0].id + + # group custom attributes + attrs = await admin_project.customattributes.list() + assert len(attrs) == 0 + attr = await admin_project.customattributes.set("key", "value1") + assert len(await gl.projects.list(custom_attributes={"key": "value1"})) == 1 + assert attr.key == "key" + assert attr.value == "value1" + assert len(await admin_project.customattributes.list()) == 1 + attr = await admin_project.customattributes.set("key", "value2") + attr = await admin_project.customattributes.get("key") + assert attr.value == "value2" + assert len(await admin_project.customattributes.list()) == 1 + await attr.delete() + assert len(await admin_project.customattributes.list()) == 0 + + # project pages domains + domain = await admin_project.pagesdomains.create({"domain": "foo.domain.com"}) + assert len(await admin_project.pagesdomains.list()) == 1 + assert len(await gl.pagesdomains.list()) == 1 + domain = await admin_project.pagesdomains.get("foo.domain.com") + assert domain.domain == "foo.domain.com" + await domain.delete() + assert len(await admin_project.pagesdomains.list()) == 0 + + # project content (files) + await admin_project.files.create( + { + "file_path": "README", + "branch": "master", + "content": "Initial content", + "commit_message": "Initial commit", + } + ) + readme = await admin_project.files.get(file_path="README", ref="master") + readme.content = base64.b64encode(b"Improved README").decode() + await asyncio.sleep(2) + await readme.save(branch="master", commit_message="new commit") + await readme.delete(commit_message="Removing README", branch="master") + + await admin_project.files.create( + { + "file_path": "README.rst", + "branch": "master", + "content": "Initial content", + "commit_message": "New commit", + } + ) + readme = await admin_project.files.get(file_path="README.rst", ref="master") + # The first decode() is the ProjectFile method, the second one is the bytes + # object method + assert readme.decode().decode() == "Initial content" + + blame = await admin_project.files.blame(file_path="README.rst", ref="master") + + data = { + "branch": "master", + "commit_message": "blah blah blah", + "actions": [{"action": "create", "file_path": "blah", "content": "blah"}], + } + await admin_project.commits.create(data) + assert "@@" in (await (await admin_project.commits.list())[0].diff())[0]["diff"] + + # commit status + commit = (await admin_project.commits.list())[0] + # size = len(commit.statuses.list()) + # status = commit.statuses.create({"state": "success", "sha": commit.id}) + # assert len(commit.statuses.list()) == size + 1 + + # assert commit.refs() + # assert commit.merge_requests() + + # commit comment + await commit.comments.create({"note": "This is a commit comment"}) + # assert len(commit.comments.list()) == 1 + + # commit discussion + count = len(await commit.discussions.list()) + discussion = await commit.discussions.create({"body": "Discussion body"}) + # assert len(commit.discussions.list()) == (count + 1) + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await commit.discussions.get(discussion.id) + # assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await commit.discussions.get(discussion.id) + # assert len(discussion.attributes["notes"]) == 1 + + # housekeeping + await admin_project.housekeeping() + + # repository + tree = await admin_project.repository_tree() + assert len(tree) != 0 + assert tree[0]["name"] == "README.rst" + blob_id = tree[0]["id"] + blob = await admin_project.repository_raw_blob(blob_id) + assert blob.decode() == "Initial content" + archive1 = await admin_project.repository_archive() + archive2 = await admin_project.repository_archive("master") + assert archive1 == archive2 + snapshot = await admin_project.snapshot() + + # project file uploads + filename = "test.txt" + file_contents = "testing contents" + uploaded_file = await admin_project.upload(filename, file_contents) + assert uploaded_file["alt"] == filename + assert uploaded_file["url"].startswith("/uploads/") + assert uploaded_file["url"].endswith("/" + filename) + assert uploaded_file["markdown"] == "[{}]({})".format( + uploaded_file["alt"], uploaded_file["url"] + ) + + # environments + await admin_project.environments.create( + {"name": "env1", "external_url": "http://fake.env/whatever"} + ) + envs = await admin_project.environments.list() + assert len(envs) == 1 + env = envs[0] + env.external_url = "http://new.env/whatever" + await env.save() + env = (await admin_project.environments.list())[0] + assert env.external_url == "http://new.env/whatever" + await env.stop() + await env.delete() + assert len(await admin_project.environments.list()) == 0 + + # Project clusters + await admin_project.clusters.create( + { + "name": "cluster1", + "platform_kubernetes_attributes": { + "api_url": "http://url", + "token": "tokenval", + }, + } + ) + clusters = await admin_project.clusters.list() + assert len(clusters) == 1 + cluster = clusters[0] + cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} + await cluster.save() + cluster = (await admin_project.clusters.list())[0] + assert cluster.platform_kubernetes["api_url"] == "http://newurl" + await cluster.delete() + assert len(await admin_project.clusters.list()) == 0 + + # Group clusters + await group1.clusters.create( + { + "name": "cluster1", + "platform_kubernetes_attributes": { + "api_url": "http://url", + "token": "tokenval", + }, + } + ) + clusters = await group1.clusters.list() + assert len(clusters) == 1 + cluster = clusters[0] + cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} + await cluster.save() + cluster = (await group1.clusters.list())[0] + assert cluster.platform_kubernetes["api_url"] == "http://newurl" + await cluster.delete() + assert len(await group1.clusters.list()) == 0 + + # project events + await admin_project.events.list() + + # forks + fork = await admin_project.forks.create({"namespace": user1.username}) + p = await gl.projects.get(fork.id) + assert p.forked_from_project["id"] == admin_project.id + + forks = await admin_project.forks.list() + assert fork.id in map(lambda p: p.id, forks) + + # project hooks + hook = await admin_project.hooks.create({"url": "http://hook.url"}) + assert len(await admin_project.hooks.list()) == 1 + hook.note_events = True + await hook.save() + hook = await admin_project.hooks.get(hook.id) + assert hook.note_events is True + await hook.delete() + + # deploy keys + deploy_key = await admin_project.keys.create( + {"title": "foo@bar", "key": DEPLOY_KEY} + ) + project_keys = list(await admin_project.keys.list()) + assert len(project_keys) == 1 + + await sudo_project.keys.enable(deploy_key.id) + assert len(await sudo_project.keys.list()) == 1 + await sudo_project.keys.delete(deploy_key.id) + assert len(await sudo_project.keys.list()) == 0 + + # labels + # label1 = admin_project.labels.create({"name": "label1", "color": "#778899"}) + # label1 = admin_project.labels.list()[0] + # assert len(admin_project.labels.list()) == 1 + # label1.new_name = "label1updated" + # label1.save() + # assert label1.name == "label1updated" + # label1.subscribe() + # assert label1.subscribed == True + # label1.unsubscribe() + # assert label1.subscribed == False + # label1.delete() + + # milestones + m1 = await admin_project.milestones.create({"title": "milestone1"}) + assert len(await admin_project.milestones.list()) == 1 + m1.due_date = "2020-01-01T00:00:00Z" + await m1.save() + m1.state_event = "close" + await m1.save() + m1 = await admin_project.milestones.get(m1.id) + assert m1.state == "closed" + assert len(await m1.issues()) == 0 + assert len(await m1.merge_requests()) == 0 + + # issues + issue1 = await admin_project.issues.create( + {"title": "my issue 1", "milestone_id": m1.id} + ) + issue2 = await admin_project.issues.create({"title": "my issue 2"}) + issue3 = await admin_project.issues.create({"title": "my issue 3"}) + assert len(await admin_project.issues.list()) == 3 + issue3.state_event = "close" + await issue3.save() + assert len(await admin_project.issues.list(state="closed")) == 1 + assert len(await admin_project.issues.list(state="opened")) == 2 + assert len(await admin_project.issues.list(milestone="milestone1")) == 1 + assert (await (await m1.issues()).next()).title == "my issue 1" + size = len(await issue1.notes.list()) + note = await issue1.notes.create({"body": "This is an issue note"}) + assert len(await issue1.notes.list()) == size + 1 + emoji = await note.awardemojis.create({"name": "tractor"}) + assert len(await note.awardemojis.list()) == 1 + await emoji.delete() + assert len(await note.awardemojis.list()) == 0 + await note.delete() + assert len(await issue1.notes.list()) == size + assert isinstance(await issue1.user_agent_detail(), dict) + + assert (await issue1.user_agent_detail())["user_agent"] + assert await issue1.participants() + assert type(await issue1.closed_by()) == list + assert type(await issue1.related_merge_requests()) == list + + # issues labels and events + label2 = await admin_project.labels.create({"name": "label2", "color": "#aabbcc"}) + issue1.labels = ["label2"] + await issue1.save() + events = await issue1.resourcelabelevents.list() + assert events + event = await issue1.resourcelabelevents.get(events[0].id) + assert event + + size = len(await issue1.discussions.list()) + discussion = await issue1.discussions.create({"body": "Discussion body"}) + assert len(await issue1.discussions.list()) == size + 1 + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await issue1.discussions.get(discussion.id) + assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await issue1.discussions.get(discussion.id) + assert len(discussion.attributes["notes"]) == 1 + + # tags + tag1 = await admin_project.tags.create({"tag_name": "v1.0", "ref": "master"}) + assert len(await admin_project.tags.list()) == 1 + await tag1.set_release_description("Description 1") + await tag1.set_release_description("Description 2") + assert tag1.release["description"] == "Description 2" + await tag1.delete() + + # project snippet + admin_project.snippets_enabled = True + await admin_project.save() + snippet = await admin_project.snippets.create( + { + "title": "snip1", + "file_name": "foo.py", + "content": "initial content", + "visibility": gitlab.v4.objects.VISIBILITY_PRIVATE, + } + ) + + assert (await snippet.user_agent_detail())["user_agent"] + + size = len(await snippet.discussions.list()) + discussion = await snippet.discussions.create({"body": "Discussion body"}) + assert len(await snippet.discussions.list()) == size + 1 + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await snippet.discussions.get(discussion.id) + assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await snippet.discussions.get(discussion.id) + assert len(discussion.attributes["notes"]) == 1 + + snippet.file_name = "bar.py" + await snippet.save() + snippet = await admin_project.snippets.get(snippet.id) + assert (await snippet.content()).decode() == "initial content" + assert snippet.file_name == "bar.py" + size = len(await admin_project.snippets.list()) + await snippet.delete() + assert len(await admin_project.snippets.list()) == (size - 1) + + # triggers + tr1 = await admin_project.triggers.create({"description": "trigger1"}) + assert len(await admin_project.triggers.list()) == 1 + await tr1.delete() + + # variables + v1 = await admin_project.variables.create({"key": "key1", "value": "value1"}) + assert len(await admin_project.variables.list()) == 1 + v1.value = "new_value1" + await v1.save() + v1 = await admin_project.variables.get(v1.key) + assert v1.value == "new_value1" + await v1.delete() + + # branches and merges + to_merge = await admin_project.branches.create( + {"branch": "branch1", "ref": "master"} + ) + await admin_project.files.create( + { + "file_path": "README2.rst", + "branch": "branch1", + "content": "Initial content", + "commit_message": "New commit in new branch", + } + ) + mr = await admin_project.mergerequests.create( + {"source_branch": "branch1", "target_branch": "master", "title": "MR readme2"} + ) + + # discussion + size = len(await mr.discussions.list()) + discussion = await mr.discussions.create({"body": "Discussion body"}) + assert len(await mr.discussions.list()) == size + 1 + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await mr.discussions.get(discussion.id) + assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await mr.discussions.get(discussion.id) + assert len(discussion.attributes["notes"]) == 1 + + # mr labels and events + mr.labels = ["label2"] + await mr.save() + events = await mr.resourcelabelevents.list() + assert events + event = await mr.resourcelabelevents.get(events[0].id) + assert event + + # rebasing + assert await mr.rebase() + + # basic testing: only make sure that the methods exist + await mr.commits() + await mr.changes() + assert await mr.participants() + + await mr.merge() + await admin_project.branches.delete("branch1") + + try: + await mr.merge() + except gitlab.GitlabMRClosedError: + pass + + # protected branches + p_b = await admin_project.protectedbranches.create({"name": "*-stable"}) + assert p_b.name == "*-stable" + p_b = await admin_project.protectedbranches.get("*-stable") + # master is protected by default when a branch has been created + assert len(await admin_project.protectedbranches.list()) == 2 + await admin_project.protectedbranches.delete("master") + await p_b.delete() + assert len(await admin_project.protectedbranches.list()) == 0 + + # stars + await admin_project.star() + assert admin_project.star_count == 1 + await admin_project.unstar() + assert admin_project.star_count == 0 + + # project boards + # boards = admin_project.boards.list() + # assert(len(boards)) + # board = boards[0] + # lists = board.lists.list() + # begin_size = len(lists) + # last_list = lists[-1] + # last_list.position = 0 + # last_list.save() + # last_list.delete() + # lists = board.lists.list() + # assert(len(lists) == begin_size - 1) + + # project badges + badge_image = "http://example.com" + badge_link = "http://example/img.svg" + badge = await admin_project.badges.create( + {"link_url": badge_link, "image_url": badge_image} + ) + assert len(await admin_project.badges.list()) == 1 + badge.image_url = "http://another.example.com" + await badge.save() + badge = await admin_project.badges.get(badge.id) + assert badge.image_url == "http://another.example.com" + await badge.delete() + assert len(await admin_project.badges.list()) == 0 + + # project wiki + wiki_content = "Wiki page content" + wp = await admin_project.wikis.create( + {"title": "wikipage", "content": wiki_content} + ) + assert len(await admin_project.wikis.list()) == 1 + wp = await admin_project.wikis.get(wp.slug) + assert wp.content == wiki_content + # update and delete seem broken + # wp.content = 'new content' + # wp.save() + # wp.delete() + # assert(len(admin_project.wikis.list()) == 0) + + # namespaces + ns = await gl.namespaces.list(all=True) + assert len(ns) != 0 + ns = (await gl.namespaces.list(search="root", all=True))[0] + assert ns.kind == "user" + + # features + # Disabled as this fails with GitLab 11.11 + # feat = gl.features.set("foo", 30) + # assert feat.name == "foo" + # assert len(gl.features.list()) == 1 + # feat.delete() + # assert len(gl.features.list()) == 0 + + # broadcast messages + msg = await gl.broadcastmessages.create({"message": "this is the message"}) + msg.color = "#444444" + await msg.save() + msg_id = msg.id + msg = (await gl.broadcastmessages.list(all=True))[0] + assert msg.color == "#444444" + msg = await gl.broadcastmessages.get(msg_id) + assert msg.color == "#444444" + await msg.delete() + assert len(await gl.broadcastmessages.list()) == 0 + + # notification settings + settings = await gl.notificationsettings.get() + settings.level = gitlab.NOTIFICATION_LEVEL_WATCH + await settings.save() + settings = await gl.notificationsettings.get() + assert settings.level == gitlab.NOTIFICATION_LEVEL_WATCH + + # services + service = await admin_project.services.get("asana") + service.api_key = "whatever" + await service.save() + service = await admin_project.services.get("asana") + assert service.active == True + await service.delete() + service = await admin_project.services.get("asana") + assert service.active == False + + # snippets + snippets = await gl.snippets.list(all=True) + assert len(snippets) == 0 + snippet = await gl.snippets.create( + {"title": "snippet1", "file_name": "snippet1.py", "content": "import gitlab"} + ) + snippet = await gl.snippets.get(snippet.id) + snippet.title = "updated_title" + await snippet.save() + snippet = await gl.snippets.get(snippet.id) + assert snippet.title == "updated_title" + content = await snippet.content() + assert content.decode() == "import gitlab" + + assert (await snippet.user_agent_detail())["user_agent"] + + await snippet.delete() + snippets = await gl.snippets.list(all=True) + assert len(snippets) == 0 + + # user activities + await gl.user_activities.list(query_parameters={"from": "2019-01-01"}) + + # events + await gl.events.list() + + # rate limit + settings = await gl.settings.get() + settings.throttle_authenticated_api_enabled = True + settings.throttle_authenticated_api_requests_per_period = 1 + settings.throttle_authenticated_api_period_in_seconds = 3 + await settings.save() + projects = list() + for i in range(0, 20): + projects.append(await gl.projects.create({"name": str(i) + "ok"})) + + error_message = None + for i in range(20, 40): + try: + projects.append( + await gl.projects.create( + {"name": str(i) + "shouldfail"}, obey_rate_limit=False + ) + ) + except gitlab.GitlabCreateError as e: + error_message = e.error_message + break + assert "Retry later" in error_message + settings.throttle_authenticated_api_enabled = False + await settings.save() + [await current_project.delete() for current_project in projects] + + # project import/export + ex = await admin_project.exports.create({}) + await ex.refresh() + count = 0 + while ex.export_status != "finished": + await asyncio.sleep(1) + await ex.refresh() + count += 1 + if count == 10: + raise Exception("Project export taking too much time") + with open("/tmp/gitlab-export.tgz", "wb") as f: + await ex.download(streamed=True, action=f.write) + + output = await gl.projects.import_project( + open("/tmp/gitlab-export.tgz", "rb"), "imported_project" + ) + project_import = await ( + await gl.projects.get(output["id"], lazy=True) + ).imports.get() + count = 0 + while project_import.import_status != "finished": + await asyncio.sleep(1) + await project_import.refresh() + count += 1 + if count == 10: + raise Exception("Project import taking too much time") + + # project releases + release_test_project = await gl.projects.create( + {"name": "release-test-project", "initialize_with_readme": True} + ) + release_name = "Demo Release" + release_tag_name = "v1.2.3" + release_description = "release notes go here" + await release_test_project.releases.create( + { + "name": release_name, + "tag_name": release_tag_name, + "description": release_description, + "ref": "master", + } + ) + assert len(await release_test_project.releases.list()) == 1 + + # get single release + retrieved_project = await release_test_project.releases.get(release_tag_name) + assert retrieved_project.name == release_name + assert retrieved_project.tag_name == release_tag_name + assert retrieved_project.description == release_description + + # delete release + await release_test_project.releases.delete(release_tag_name) + assert len(await release_test_project.releases.list()) == 0 + await release_test_project.delete() + + # status + message = "Test" + emoji = "thumbsup" + status = await gl.user.status.get() + status.message = message + status.emoji = emoji + await status.save() + new_status = await gl.user.status.get() + assert new_status.message == message + assert new_status.emoji == emoji + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) From 1bf296819c17c162998de378f9d4955b50378512 Mon Sep 17 00:00:00 2001 From: Aleksey Shalynin Date: Fri, 21 Feb 2020 18:50:12 +0300 Subject: [PATCH 15/15] test: change tox to pytest and keep integration test file name --- tools/python_async_test_v4.py | 1003 ------------------ tools/python_test_v4.py | 1846 +++++++++++++++++---------------- tox.ini | 2 +- 3 files changed, 941 insertions(+), 1910 deletions(-) delete mode 100644 tools/python_async_test_v4.py diff --git a/tools/python_async_test_v4.py b/tools/python_async_test_v4.py deleted file mode 100644 index 021fa730b..000000000 --- a/tools/python_async_test_v4.py +++ /dev/null @@ -1,1003 +0,0 @@ -import asyncio -import base64 -import os - -import httpx - -import gitlab - -LOGIN = "root" -PASSWORD = "5iveL!fe" - -SSH_KEY = ( - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih" - "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n" - "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l" - "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI" - "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh" - "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar" -) -DEPLOY_KEY = ( - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG" - "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI" - "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6" - "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu" - "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv" - "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc" - "vn bar@foo" -) - -GPG_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBFn5mzYBCADH6SDVPAp1zh/hxmTi0QplkOfExBACpuY6OhzNdIg+8/528b3g -Y5YFR6T/HLv/PmeHskUj21end1C0PNG2T9dTx+2Vlh9ISsSG1kyF9T5fvMR3bE0x -Dl6S489CXZrjPTS9SHk1kF+7dwjUxLJyxF9hPiSihFefDFu3NeOtG/u8vbC1mewQ -ZyAYue+mqtqcCIFFoBz7wHKMWjIVSJSyTkXExu4OzpVvy3l2EikbvavI3qNz84b+ -Mgkv/kiBlNoCy3CVuPk99RYKZ3lX1vVtqQ0OgNGQvb4DjcpyjmbKyibuZwhDjIOh -au6d1OyEbayTntd+dQ4j9EMSnEvm/0MJ4eXPABEBAAG0G0dpdGxhYlRlc3QxIDxm -YWtlQGZha2UudGxkPokBNwQTAQgAIQUCWfmbNgIbAwULCQgHAgYVCAkKCwIEFgID -AQIeAQIXgAAKCRBgxELHf8f3hF3yB/wNJlWPKY65UsB4Lo0hs1OxdxCDqXogSi0u -6crDEIiyOte62pNZKzWy8TJcGZvznRTZ7t8hXgKFLz3PRMcl+vAiRC6quIDUj+2V -eYfwaItd1lUfzvdCaC7Venf4TQ74f5vvNg/zoGwE6eRoSbjlLv9nqsxeA0rUBUQL -LYikWhVMP3TrlfgfduYvh6mfgh57BDLJ9kJVpyfxxx9YLKZbaas9sPa6LgBtR555 -JziUxHmbEv8XCsUU8uoFeP1pImbNBplqE3wzJwzOMSmmch7iZzrAwfN7N2j3Wj0H -B5kQddJ9dmB4BbU0IXGhWczvdpxboI2wdY8a1JypxOdePoph/43iuQENBFn5mzYB -CADnTPY0Zf3d9zLjBNgIb3yDl94uOcKCq0twNmyjMhHzGqw+UMe9BScy34GL94Al -xFRQoaL+7P8hGsnsNku29A/VDZivcI+uxTx4WQ7OLcn7V0bnHV4d76iky2ufbUt/ -GofthjDs1SonePO2N09sS4V4uK0d5N4BfCzzXgvg8etCLxNmC9BGt7AaKUUzKBO4 -2QvNNaC2C/8XEnOgNWYvR36ylAXAmo0sGFXUsBCTiq1fugS9pwtaS2JmaVpZZ3YT -pMZlS0+SjC5BZYFqSmKCsA58oBRzCxQz57nR4h5VEflgD+Hy0HdW0UHETwz83E6/ -U0LL6YyvhwFr6KPq5GxinSvfABEBAAGJAR8EGAEIAAkFAln5mzYCGwwACgkQYMRC -x3/H94SJgwgAlKQb10/xcL/epdDkR7vbiei7huGLBpRDb/L5fM8B5W77Qi8Xmuqj -cCu1j99ZCA5hs/vwVn8j8iLSBGMC5gxcuaar/wtmiaEvT9fO/h6q4opG7NcuiJ8H -wRj8ccJmRssNqDD913PLz7T40Ts62blhrEAlJozGVG/q7T3RAZcskOUHKeHfc2RI -YzGsC/I9d7k6uxAv1L9Nm5F2HaAQDzhkdd16nKkGaPGR35cT1JLInkfl5cdm7ldN -nxs4TLO3kZjUTgWKdhpgRNF5hwaz51ZjpebaRf/ZqRuNyX4lIRolDxzOn/+O1o8L -qG2ZdhHHmSK2LaQLFiSprUkikStNU9BqSQ== -=5OGa ------END PGP PUBLIC KEY BLOCK-----""" -AVATAR_PATH = os.path.join(os.path.dirname(__file__), "avatar.png") - - -async def main(): - # token authentication from config file - gl = gitlab.Gitlab.from_config(config_files=["/tmp/python-gitlab.cfg"]) - gl.enable_debug() - await gl.auth() - assert isinstance(gl.user, gitlab.v4.objects.CurrentUser) - - # markdown - html = await gl.markdown("foo") - assert "foo" in html - - success, errors = await gl.lint("Invalid") - assert success is False - assert errors - - # sidekiq - out = await gl.sidekiq.queue_metrics() - assert isinstance(out, dict) - assert "pages" in out["queues"] - out = await gl.sidekiq.process_metrics() - assert isinstance(out, dict) - assert "hostname" in out["processes"][0] - out = await gl.sidekiq.job_stats() - assert isinstance(out, dict) - assert "processed" in out["jobs"] - out = await gl.sidekiq.compound_metrics() - assert isinstance(out, dict) - assert "jobs" in out - assert "processes" in out - assert "queues" in out - - # settings - settings = await gl.settings.get() - settings.default_projects_limit = 42 - await settings.save() - settings = await gl.settings.get() - assert settings.default_projects_limit == 42 - - # users - new_user = await gl.users.create( - { - "email": "foo@bar.com", - "username": "foo", - "name": "foo", - "password": "foo_password", - "avatar": open(AVATAR_PATH, "rb"), - } - ) - avatar_url = new_user.avatar_url.replace("gitlab.test", "localhost:8080") - uploaded_avatar = httpx.get(avatar_url).content - assert uploaded_avatar == open(AVATAR_PATH, "rb").read() - users_list = await gl.users.list() - for user in users_list: - if user.username == "foo": - break - assert new_user.username == user.username - assert new_user.email == user.email - - await new_user.block() - await new_user.unblock() - - # user projects list - assert len(await new_user.projects.list()) == 0 - - # events list - await new_user.events.list() - - foobar_user = await gl.users.create( - { - "email": "foobar@example.com", - "username": "foobar", - "name": "Foo Bar", - "password": "foobar_password", - } - ) - - assert (await gl.users.list(search="foobar"))[0].id == foobar_user.id - expected = [new_user, foobar_user] - actual = list(await gl.users.list(search="foo")) - assert len(expected) == len(actual) - assert len(await gl.users.list(search="asdf")) == 0 - foobar_user.bio = "This is the user bio" - await foobar_user.save() - - # GPG keys - gkey = await new_user.gpgkeys.create({"key": GPG_KEY}) - assert len(await new_user.gpgkeys.list()) == 1 - # Seems broken on the gitlab side - # gkey = new_user.gpgkeys.get(gkey.id) - await gkey.delete() - assert len(await new_user.gpgkeys.list()) == 0 - - # SSH keys - key = await new_user.keys.create({"title": "testkey", "key": SSH_KEY}) - assert len(await new_user.keys.list()) == 1 - await key.delete() - assert len(await new_user.keys.list()) == 0 - - # emails - email = await new_user.emails.create({"email": "foo2@bar.com"}) - assert len(await new_user.emails.list()) == 1 - await email.delete() - assert len(await new_user.emails.list()) == 0 - - # custom attributes - attrs = await new_user.customattributes.list() - assert len(attrs) == 0 - attr = await new_user.customattributes.set("key", "value1") - assert len(await gl.users.list(custom_attributes={"key": "value1"})) == 1 - assert attr.key == "key" - assert attr.value == "value1" - assert len(await new_user.customattributes.list()) == 1 - attr = await new_user.customattributes.set("key", "value2") - attr = await new_user.customattributes.get("key") - assert attr.value == "value2" - assert len(await new_user.customattributes.list()) == 1 - await attr.delete() - assert len(await new_user.customattributes.list()) == 0 - - # impersonation tokens - user_token = await new_user.impersonationtokens.create( - {"name": "token1", "scopes": ["api", "read_user"]} - ) - l = await new_user.impersonationtokens.list(state="active") - assert len(l) == 1 - await user_token.delete() - l = await new_user.impersonationtokens.list(state="active") - assert len(l) == 0 - l = await new_user.impersonationtokens.list(state="inactive") - assert len(l) == 1 - - await new_user.delete() - await foobar_user.delete() - assert len(await gl.users.list()) == 3 + len( - [u for u in await gl.users.list() if u.username == "ghost"] - ) - - # current user mail - mail = await gl.user.emails.create({"email": "current@user.com"}) - assert len(await gl.user.emails.list()) == 1 - await mail.delete() - assert len(await gl.user.emails.list()) == 0 - - # current user GPG keys - gkey = await gl.user.gpgkeys.create({"key": GPG_KEY}) - assert len(await gl.user.gpgkeys.list()) == 1 - # Seems broken on the gitlab side - gkey = await gl.user.gpgkeys.get(gkey.id) - await gkey.delete() - assert len(await gl.user.gpgkeys.list()) == 0 - - # current user key - key = await gl.user.keys.create({"title": "testkey", "key": SSH_KEY}) - assert len(await gl.user.keys.list()) == 1 - await key.delete() - assert len(await gl.user.keys.list()) == 0 - - # templates - assert await gl.dockerfiles.list() - dockerfile = await gl.dockerfiles.get("Node") - assert dockerfile.content is not None - - assert await gl.gitignores.list() - gitignore = await gl.gitignores.get("Node") - assert gitignore.content is not None - - assert await gl.gitlabciymls.list() - gitlabciyml = await gl.gitlabciymls.get("Nodejs") - assert gitlabciyml.content is not None - - assert await gl.licenses.list() - license = await gl.licenses.get( - "bsd-2-clause", project="mytestproject", fullname="mytestfullname" - ) - assert "mytestfullname" in license.content - - # groups - user1 = await gl.users.create( - { - "email": "user1@test.com", - "username": "user1", - "name": "user1", - "password": "user1_pass", - } - ) - user2 = await gl.users.create( - { - "email": "user2@test.com", - "username": "user2", - "name": "user2", - "password": "user2_pass", - } - ) - group1 = await gl.groups.create({"name": "group1", "path": "group1"}) - group2 = await gl.groups.create({"name": "group2", "path": "group2"}) - - p_id = (await gl.groups.list(search="group2"))[0].id - group3 = await gl.groups.create( - {"name": "group3", "path": "group3", "parent_id": p_id} - ) - - assert len(await gl.groups.list()) == 3 - assert len(await gl.groups.list(search="oup1")) == 1 - assert group3.parent_id == p_id - assert (await group2.subgroups.list())[0].id == group3.id - - await group1.members.create( - {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user1.id} - ) - await group1.members.create( - {"access_level": gitlab.const.GUEST_ACCESS, "user_id": user2.id} - ) - - await group2.members.create( - {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user2.id} - ) - - # Administrator belongs to the groups - assert len(await group1.members.list()) == 3 - assert len(await group2.members.list()) == 2 - - await group1.members.delete(user1.id) - assert len(await group1.members.list()) == 2 - assert len(await group1.members.all()) - member = await group1.members.get(user2.id) - member.access_level = gitlab.const.OWNER_ACCESS - await member.save() - member = await group1.members.get(user2.id) - assert member.access_level == gitlab.const.OWNER_ACCESS - - await group2.members.delete(gl.user.id) - - # group custom attributes - attrs = await group2.customattributes.list() - assert len(attrs) == 0 - attr = await group2.customattributes.set("key", "value1") - assert len(await gl.groups.list(custom_attributes={"key": "value1"})) == 1 - assert attr.key == "key" - assert attr.value == "value1" - assert len(await group2.customattributes.list()) == 1 - attr = await group2.customattributes.set("key", "value2") - attr = await group2.customattributes.get("key") - assert attr.value == "value2" - assert len(await group2.customattributes.list()) == 1 - await attr.delete() - assert len(await group2.customattributes.list()) == 0 - - # group notification settings - settings = await group2.notificationsettings.get() - settings.level = "disabled" - await settings.save() - settings = await group2.notificationsettings.get() - assert settings.level == "disabled" - - # group badges - badge_image = "http://example.com" - badge_link = "http://example/img.svg" - badge = await group2.badges.create( - {"link_url": badge_link, "image_url": badge_image} - ) - assert len(await group2.badges.list()) == 1 - badge.image_url = "http://another.example.com" - await badge.save() - badge = await group2.badges.get(badge.id) - assert badge.image_url == "http://another.example.com" - await badge.delete() - assert len(await group2.badges.list()) == 0 - - # group milestones - gm1 = await group1.milestones.create({"title": "groupmilestone1"}) - assert len(await group1.milestones.list()) == 1 - gm1.due_date = "2020-01-01T00:00:00Z" - await gm1.save() - gm1.state_event = "close" - await gm1.save() - gm1 = await group1.milestones.get(gm1.id) - assert gm1.state == "closed" - assert len(await gm1.issues()) == 0 - assert len(await gm1.merge_requests()) == 0 - - # group variables - await group1.variables.create({"key": "foo", "value": "bar"}) - g_v = await group1.variables.get("foo") - assert g_v.value == "bar" - g_v.value = "baz" - await g_v.save() - g_v = await group1.variables.get("foo") - assert g_v.value == "baz" - assert len(await group1.variables.list()) == 1 - await g_v.delete() - assert len(await group1.variables.list()) == 0 - - # group labels - # group1.labels.create({"name": "foo", "description": "bar", "color": "#112233"}) - # g_l = group1.labels.get("foo") - # assert g_l.description == "bar" - # g_l.description = "baz" - # g_l.save() - # g_l = group1.labels.get("foo") - # assert g_l.description == "baz" - # assert len(group1.labels.list()) == 1 - # g_l.delete() - # assert len(group1.labels.list()) == 0 - - # hooks - hook = await gl.hooks.create({"url": "http://whatever.com"}) - assert len(await gl.hooks.list()) == 1 - await hook.delete() - assert len(await gl.hooks.list()) == 0 - - # projects - admin_project = await gl.projects.create({"name": "admin_project"}) - gr1_project = await gl.projects.create( - {"name": "gr1_project", "namespace_id": group1.id} - ) - gr2_project = await gl.projects.create( - {"name": "gr2_project", "namespace_id": group2.id} - ) - sudo_project = await gl.projects.create({"name": "sudo_project"}, sudo=user1.name) - - assert len(await gl.projects.list(owned=True)) == 2 - assert len(await gl.projects.list(search="admin")) == 1 - - # test pagination - l1 = await gl.projects.list(per_page=1, page=1) - l2 = await gl.projects.list(per_page=1, page=2) - assert len(l1) == 1 - assert len(l2) == 1 - assert l1[0].id != l2[0].id - - # group custom attributes - attrs = await admin_project.customattributes.list() - assert len(attrs) == 0 - attr = await admin_project.customattributes.set("key", "value1") - assert len(await gl.projects.list(custom_attributes={"key": "value1"})) == 1 - assert attr.key == "key" - assert attr.value == "value1" - assert len(await admin_project.customattributes.list()) == 1 - attr = await admin_project.customattributes.set("key", "value2") - attr = await admin_project.customattributes.get("key") - assert attr.value == "value2" - assert len(await admin_project.customattributes.list()) == 1 - await attr.delete() - assert len(await admin_project.customattributes.list()) == 0 - - # project pages domains - domain = await admin_project.pagesdomains.create({"domain": "foo.domain.com"}) - assert len(await admin_project.pagesdomains.list()) == 1 - assert len(await gl.pagesdomains.list()) == 1 - domain = await admin_project.pagesdomains.get("foo.domain.com") - assert domain.domain == "foo.domain.com" - await domain.delete() - assert len(await admin_project.pagesdomains.list()) == 0 - - # project content (files) - await admin_project.files.create( - { - "file_path": "README", - "branch": "master", - "content": "Initial content", - "commit_message": "Initial commit", - } - ) - readme = await admin_project.files.get(file_path="README", ref="master") - readme.content = base64.b64encode(b"Improved README").decode() - await asyncio.sleep(2) - await readme.save(branch="master", commit_message="new commit") - await readme.delete(commit_message="Removing README", branch="master") - - await admin_project.files.create( - { - "file_path": "README.rst", - "branch": "master", - "content": "Initial content", - "commit_message": "New commit", - } - ) - readme = await admin_project.files.get(file_path="README.rst", ref="master") - # The first decode() is the ProjectFile method, the second one is the bytes - # object method - assert readme.decode().decode() == "Initial content" - - blame = await admin_project.files.blame(file_path="README.rst", ref="master") - - data = { - "branch": "master", - "commit_message": "blah blah blah", - "actions": [{"action": "create", "file_path": "blah", "content": "blah"}], - } - await admin_project.commits.create(data) - assert "@@" in (await (await admin_project.commits.list())[0].diff())[0]["diff"] - - # commit status - commit = (await admin_project.commits.list())[0] - # size = len(commit.statuses.list()) - # status = commit.statuses.create({"state": "success", "sha": commit.id}) - # assert len(commit.statuses.list()) == size + 1 - - # assert commit.refs() - # assert commit.merge_requests() - - # commit comment - await commit.comments.create({"note": "This is a commit comment"}) - # assert len(commit.comments.list()) == 1 - - # commit discussion - count = len(await commit.discussions.list()) - discussion = await commit.discussions.create({"body": "Discussion body"}) - # assert len(commit.discussions.list()) == (count + 1) - d_note = await discussion.notes.create({"body": "first note"}) - d_note_from_get = await discussion.notes.get(d_note.id) - d_note_from_get.body = "updated body" - await d_note_from_get.save() - discussion = await commit.discussions.get(discussion.id) - # assert discussion.attributes["notes"][-1]["body"] == "updated body" - await d_note_from_get.delete() - discussion = await commit.discussions.get(discussion.id) - # assert len(discussion.attributes["notes"]) == 1 - - # housekeeping - await admin_project.housekeeping() - - # repository - tree = await admin_project.repository_tree() - assert len(tree) != 0 - assert tree[0]["name"] == "README.rst" - blob_id = tree[0]["id"] - blob = await admin_project.repository_raw_blob(blob_id) - assert blob.decode() == "Initial content" - archive1 = await admin_project.repository_archive() - archive2 = await admin_project.repository_archive("master") - assert archive1 == archive2 - snapshot = await admin_project.snapshot() - - # project file uploads - filename = "test.txt" - file_contents = "testing contents" - uploaded_file = await admin_project.upload(filename, file_contents) - assert uploaded_file["alt"] == filename - assert uploaded_file["url"].startswith("/uploads/") - assert uploaded_file["url"].endswith("/" + filename) - assert uploaded_file["markdown"] == "[{}]({})".format( - uploaded_file["alt"], uploaded_file["url"] - ) - - # environments - await admin_project.environments.create( - {"name": "env1", "external_url": "http://fake.env/whatever"} - ) - envs = await admin_project.environments.list() - assert len(envs) == 1 - env = envs[0] - env.external_url = "http://new.env/whatever" - await env.save() - env = (await admin_project.environments.list())[0] - assert env.external_url == "http://new.env/whatever" - await env.stop() - await env.delete() - assert len(await admin_project.environments.list()) == 0 - - # Project clusters - await admin_project.clusters.create( - { - "name": "cluster1", - "platform_kubernetes_attributes": { - "api_url": "http://url", - "token": "tokenval", - }, - } - ) - clusters = await admin_project.clusters.list() - assert len(clusters) == 1 - cluster = clusters[0] - cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} - await cluster.save() - cluster = (await admin_project.clusters.list())[0] - assert cluster.platform_kubernetes["api_url"] == "http://newurl" - await cluster.delete() - assert len(await admin_project.clusters.list()) == 0 - - # Group clusters - await group1.clusters.create( - { - "name": "cluster1", - "platform_kubernetes_attributes": { - "api_url": "http://url", - "token": "tokenval", - }, - } - ) - clusters = await group1.clusters.list() - assert len(clusters) == 1 - cluster = clusters[0] - cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} - await cluster.save() - cluster = (await group1.clusters.list())[0] - assert cluster.platform_kubernetes["api_url"] == "http://newurl" - await cluster.delete() - assert len(await group1.clusters.list()) == 0 - - # project events - await admin_project.events.list() - - # forks - fork = await admin_project.forks.create({"namespace": user1.username}) - p = await gl.projects.get(fork.id) - assert p.forked_from_project["id"] == admin_project.id - - forks = await admin_project.forks.list() - assert fork.id in map(lambda p: p.id, forks) - - # project hooks - hook = await admin_project.hooks.create({"url": "http://hook.url"}) - assert len(await admin_project.hooks.list()) == 1 - hook.note_events = True - await hook.save() - hook = await admin_project.hooks.get(hook.id) - assert hook.note_events is True - await hook.delete() - - # deploy keys - deploy_key = await admin_project.keys.create( - {"title": "foo@bar", "key": DEPLOY_KEY} - ) - project_keys = list(await admin_project.keys.list()) - assert len(project_keys) == 1 - - await sudo_project.keys.enable(deploy_key.id) - assert len(await sudo_project.keys.list()) == 1 - await sudo_project.keys.delete(deploy_key.id) - assert len(await sudo_project.keys.list()) == 0 - - # labels - # label1 = admin_project.labels.create({"name": "label1", "color": "#778899"}) - # label1 = admin_project.labels.list()[0] - # assert len(admin_project.labels.list()) == 1 - # label1.new_name = "label1updated" - # label1.save() - # assert label1.name == "label1updated" - # label1.subscribe() - # assert label1.subscribed == True - # label1.unsubscribe() - # assert label1.subscribed == False - # label1.delete() - - # milestones - m1 = await admin_project.milestones.create({"title": "milestone1"}) - assert len(await admin_project.milestones.list()) == 1 - m1.due_date = "2020-01-01T00:00:00Z" - await m1.save() - m1.state_event = "close" - await m1.save() - m1 = await admin_project.milestones.get(m1.id) - assert m1.state == "closed" - assert len(await m1.issues()) == 0 - assert len(await m1.merge_requests()) == 0 - - # issues - issue1 = await admin_project.issues.create( - {"title": "my issue 1", "milestone_id": m1.id} - ) - issue2 = await admin_project.issues.create({"title": "my issue 2"}) - issue3 = await admin_project.issues.create({"title": "my issue 3"}) - assert len(await admin_project.issues.list()) == 3 - issue3.state_event = "close" - await issue3.save() - assert len(await admin_project.issues.list(state="closed")) == 1 - assert len(await admin_project.issues.list(state="opened")) == 2 - assert len(await admin_project.issues.list(milestone="milestone1")) == 1 - assert (await (await m1.issues()).next()).title == "my issue 1" - size = len(await issue1.notes.list()) - note = await issue1.notes.create({"body": "This is an issue note"}) - assert len(await issue1.notes.list()) == size + 1 - emoji = await note.awardemojis.create({"name": "tractor"}) - assert len(await note.awardemojis.list()) == 1 - await emoji.delete() - assert len(await note.awardemojis.list()) == 0 - await note.delete() - assert len(await issue1.notes.list()) == size - assert isinstance(await issue1.user_agent_detail(), dict) - - assert (await issue1.user_agent_detail())["user_agent"] - assert await issue1.participants() - assert type(await issue1.closed_by()) == list - assert type(await issue1.related_merge_requests()) == list - - # issues labels and events - label2 = await admin_project.labels.create({"name": "label2", "color": "#aabbcc"}) - issue1.labels = ["label2"] - await issue1.save() - events = await issue1.resourcelabelevents.list() - assert events - event = await issue1.resourcelabelevents.get(events[0].id) - assert event - - size = len(await issue1.discussions.list()) - discussion = await issue1.discussions.create({"body": "Discussion body"}) - assert len(await issue1.discussions.list()) == size + 1 - d_note = await discussion.notes.create({"body": "first note"}) - d_note_from_get = await discussion.notes.get(d_note.id) - d_note_from_get.body = "updated body" - await d_note_from_get.save() - discussion = await issue1.discussions.get(discussion.id) - assert discussion.attributes["notes"][-1]["body"] == "updated body" - await d_note_from_get.delete() - discussion = await issue1.discussions.get(discussion.id) - assert len(discussion.attributes["notes"]) == 1 - - # tags - tag1 = await admin_project.tags.create({"tag_name": "v1.0", "ref": "master"}) - assert len(await admin_project.tags.list()) == 1 - await tag1.set_release_description("Description 1") - await tag1.set_release_description("Description 2") - assert tag1.release["description"] == "Description 2" - await tag1.delete() - - # project snippet - admin_project.snippets_enabled = True - await admin_project.save() - snippet = await admin_project.snippets.create( - { - "title": "snip1", - "file_name": "foo.py", - "content": "initial content", - "visibility": gitlab.v4.objects.VISIBILITY_PRIVATE, - } - ) - - assert (await snippet.user_agent_detail())["user_agent"] - - size = len(await snippet.discussions.list()) - discussion = await snippet.discussions.create({"body": "Discussion body"}) - assert len(await snippet.discussions.list()) == size + 1 - d_note = await discussion.notes.create({"body": "first note"}) - d_note_from_get = await discussion.notes.get(d_note.id) - d_note_from_get.body = "updated body" - await d_note_from_get.save() - discussion = await snippet.discussions.get(discussion.id) - assert discussion.attributes["notes"][-1]["body"] == "updated body" - await d_note_from_get.delete() - discussion = await snippet.discussions.get(discussion.id) - assert len(discussion.attributes["notes"]) == 1 - - snippet.file_name = "bar.py" - await snippet.save() - snippet = await admin_project.snippets.get(snippet.id) - assert (await snippet.content()).decode() == "initial content" - assert snippet.file_name == "bar.py" - size = len(await admin_project.snippets.list()) - await snippet.delete() - assert len(await admin_project.snippets.list()) == (size - 1) - - # triggers - tr1 = await admin_project.triggers.create({"description": "trigger1"}) - assert len(await admin_project.triggers.list()) == 1 - await tr1.delete() - - # variables - v1 = await admin_project.variables.create({"key": "key1", "value": "value1"}) - assert len(await admin_project.variables.list()) == 1 - v1.value = "new_value1" - await v1.save() - v1 = await admin_project.variables.get(v1.key) - assert v1.value == "new_value1" - await v1.delete() - - # branches and merges - to_merge = await admin_project.branches.create( - {"branch": "branch1", "ref": "master"} - ) - await admin_project.files.create( - { - "file_path": "README2.rst", - "branch": "branch1", - "content": "Initial content", - "commit_message": "New commit in new branch", - } - ) - mr = await admin_project.mergerequests.create( - {"source_branch": "branch1", "target_branch": "master", "title": "MR readme2"} - ) - - # discussion - size = len(await mr.discussions.list()) - discussion = await mr.discussions.create({"body": "Discussion body"}) - assert len(await mr.discussions.list()) == size + 1 - d_note = await discussion.notes.create({"body": "first note"}) - d_note_from_get = await discussion.notes.get(d_note.id) - d_note_from_get.body = "updated body" - await d_note_from_get.save() - discussion = await mr.discussions.get(discussion.id) - assert discussion.attributes["notes"][-1]["body"] == "updated body" - await d_note_from_get.delete() - discussion = await mr.discussions.get(discussion.id) - assert len(discussion.attributes["notes"]) == 1 - - # mr labels and events - mr.labels = ["label2"] - await mr.save() - events = await mr.resourcelabelevents.list() - assert events - event = await mr.resourcelabelevents.get(events[0].id) - assert event - - # rebasing - assert await mr.rebase() - - # basic testing: only make sure that the methods exist - await mr.commits() - await mr.changes() - assert await mr.participants() - - await mr.merge() - await admin_project.branches.delete("branch1") - - try: - await mr.merge() - except gitlab.GitlabMRClosedError: - pass - - # protected branches - p_b = await admin_project.protectedbranches.create({"name": "*-stable"}) - assert p_b.name == "*-stable" - p_b = await admin_project.protectedbranches.get("*-stable") - # master is protected by default when a branch has been created - assert len(await admin_project.protectedbranches.list()) == 2 - await admin_project.protectedbranches.delete("master") - await p_b.delete() - assert len(await admin_project.protectedbranches.list()) == 0 - - # stars - await admin_project.star() - assert admin_project.star_count == 1 - await admin_project.unstar() - assert admin_project.star_count == 0 - - # project boards - # boards = admin_project.boards.list() - # assert(len(boards)) - # board = boards[0] - # lists = board.lists.list() - # begin_size = len(lists) - # last_list = lists[-1] - # last_list.position = 0 - # last_list.save() - # last_list.delete() - # lists = board.lists.list() - # assert(len(lists) == begin_size - 1) - - # project badges - badge_image = "http://example.com" - badge_link = "http://example/img.svg" - badge = await admin_project.badges.create( - {"link_url": badge_link, "image_url": badge_image} - ) - assert len(await admin_project.badges.list()) == 1 - badge.image_url = "http://another.example.com" - await badge.save() - badge = await admin_project.badges.get(badge.id) - assert badge.image_url == "http://another.example.com" - await badge.delete() - assert len(await admin_project.badges.list()) == 0 - - # project wiki - wiki_content = "Wiki page content" - wp = await admin_project.wikis.create( - {"title": "wikipage", "content": wiki_content} - ) - assert len(await admin_project.wikis.list()) == 1 - wp = await admin_project.wikis.get(wp.slug) - assert wp.content == wiki_content - # update and delete seem broken - # wp.content = 'new content' - # wp.save() - # wp.delete() - # assert(len(admin_project.wikis.list()) == 0) - - # namespaces - ns = await gl.namespaces.list(all=True) - assert len(ns) != 0 - ns = (await gl.namespaces.list(search="root", all=True))[0] - assert ns.kind == "user" - - # features - # Disabled as this fails with GitLab 11.11 - # feat = gl.features.set("foo", 30) - # assert feat.name == "foo" - # assert len(gl.features.list()) == 1 - # feat.delete() - # assert len(gl.features.list()) == 0 - - # broadcast messages - msg = await gl.broadcastmessages.create({"message": "this is the message"}) - msg.color = "#444444" - await msg.save() - msg_id = msg.id - msg = (await gl.broadcastmessages.list(all=True))[0] - assert msg.color == "#444444" - msg = await gl.broadcastmessages.get(msg_id) - assert msg.color == "#444444" - await msg.delete() - assert len(await gl.broadcastmessages.list()) == 0 - - # notification settings - settings = await gl.notificationsettings.get() - settings.level = gitlab.NOTIFICATION_LEVEL_WATCH - await settings.save() - settings = await gl.notificationsettings.get() - assert settings.level == gitlab.NOTIFICATION_LEVEL_WATCH - - # services - service = await admin_project.services.get("asana") - service.api_key = "whatever" - await service.save() - service = await admin_project.services.get("asana") - assert service.active == True - await service.delete() - service = await admin_project.services.get("asana") - assert service.active == False - - # snippets - snippets = await gl.snippets.list(all=True) - assert len(snippets) == 0 - snippet = await gl.snippets.create( - {"title": "snippet1", "file_name": "snippet1.py", "content": "import gitlab"} - ) - snippet = await gl.snippets.get(snippet.id) - snippet.title = "updated_title" - await snippet.save() - snippet = await gl.snippets.get(snippet.id) - assert snippet.title == "updated_title" - content = await snippet.content() - assert content.decode() == "import gitlab" - - assert (await snippet.user_agent_detail())["user_agent"] - - await snippet.delete() - snippets = await gl.snippets.list(all=True) - assert len(snippets) == 0 - - # user activities - await gl.user_activities.list(query_parameters={"from": "2019-01-01"}) - - # events - await gl.events.list() - - # rate limit - settings = await gl.settings.get() - settings.throttle_authenticated_api_enabled = True - settings.throttle_authenticated_api_requests_per_period = 1 - settings.throttle_authenticated_api_period_in_seconds = 3 - await settings.save() - projects = list() - for i in range(0, 20): - projects.append(await gl.projects.create({"name": str(i) + "ok"})) - - error_message = None - for i in range(20, 40): - try: - projects.append( - await gl.projects.create( - {"name": str(i) + "shouldfail"}, obey_rate_limit=False - ) - ) - except gitlab.GitlabCreateError as e: - error_message = e.error_message - break - assert "Retry later" in error_message - settings.throttle_authenticated_api_enabled = False - await settings.save() - [await current_project.delete() for current_project in projects] - - # project import/export - ex = await admin_project.exports.create({}) - await ex.refresh() - count = 0 - while ex.export_status != "finished": - await asyncio.sleep(1) - await ex.refresh() - count += 1 - if count == 10: - raise Exception("Project export taking too much time") - with open("/tmp/gitlab-export.tgz", "wb") as f: - await ex.download(streamed=True, action=f.write) - - output = await gl.projects.import_project( - open("/tmp/gitlab-export.tgz", "rb"), "imported_project" - ) - project_import = await ( - await gl.projects.get(output["id"], lazy=True) - ).imports.get() - count = 0 - while project_import.import_status != "finished": - await asyncio.sleep(1) - await project_import.refresh() - count += 1 - if count == 10: - raise Exception("Project import taking too much time") - - # project releases - release_test_project = await gl.projects.create( - {"name": "release-test-project", "initialize_with_readme": True} - ) - release_name = "Demo Release" - release_tag_name = "v1.2.3" - release_description = "release notes go here" - await release_test_project.releases.create( - { - "name": release_name, - "tag_name": release_tag_name, - "description": release_description, - "ref": "master", - } - ) - assert len(await release_test_project.releases.list()) == 1 - - # get single release - retrieved_project = await release_test_project.releases.get(release_tag_name) - assert retrieved_project.name == release_name - assert retrieved_project.tag_name == release_tag_name - assert retrieved_project.description == release_description - - # delete release - await release_test_project.releases.delete(release_tag_name) - assert len(await release_test_project.releases.list()) == 0 - await release_test_project.delete() - - # status - message = "Test" - emoji = "thumbsup" - status = await gl.user.status.get() - status.message = message - status.emoji = emoji - await status.save() - new_status = await gl.user.status.get() - assert new_status.message == message - assert new_status.emoji == emoji - - -if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index bffdd2a17..021fa730b 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -1,8 +1,8 @@ +import asyncio import base64 import os -import time -import requests +import httpx import gitlab @@ -59,911 +59,945 @@ AVATAR_PATH = os.path.join(os.path.dirname(__file__), "avatar.png") -# token authentication from config file -gl = gitlab.Gitlab.from_config(config_files=["/tmp/python-gitlab.cfg"]) -gl.auth() -assert isinstance(gl.user, gitlab.v4.objects.CurrentUser) - -# markdown -html = gl.markdown("foo") -assert "foo" in html - -success, errors = gl.lint("Invalid") -assert success is False -assert errors - -# sidekiq -out = gl.sidekiq.queue_metrics() -assert isinstance(out, dict) -assert "pages" in out["queues"] -out = gl.sidekiq.process_metrics() -assert isinstance(out, dict) -assert "hostname" in out["processes"][0] -out = gl.sidekiq.job_stats() -assert isinstance(out, dict) -assert "processed" in out["jobs"] -out = gl.sidekiq.compound_metrics() -assert isinstance(out, dict) -assert "jobs" in out -assert "processes" in out -assert "queues" in out - -# settings -settings = gl.settings.get() -settings.default_projects_limit = 42 -settings.save() -settings = gl.settings.get() -assert settings.default_projects_limit == 42 - -# users -new_user = gl.users.create( - { - "email": "foo@bar.com", - "username": "foo", - "name": "foo", - "password": "foo_password", - "avatar": open(AVATAR_PATH, "rb"), - } -) -avatar_url = new_user.avatar_url.replace("gitlab.test", "localhost:8080") -uploaded_avatar = requests.get(avatar_url).content -assert uploaded_avatar == open(AVATAR_PATH, "rb").read() -users_list = gl.users.list() -for user in users_list: - if user.username == "foo": - break -assert new_user.username == user.username -assert new_user.email == user.email - -new_user.block() -new_user.unblock() - -# user projects list -assert len(new_user.projects.list()) == 0 - -# events list -new_user.events.list() - -foobar_user = gl.users.create( - { - "email": "foobar@example.com", - "username": "foobar", - "name": "Foo Bar", - "password": "foobar_password", - } -) - -assert gl.users.list(search="foobar")[0].id == foobar_user.id -expected = [new_user, foobar_user] -actual = list(gl.users.list(search="foo")) -assert len(expected) == len(actual) -assert len(gl.users.list(search="asdf")) == 0 -foobar_user.bio = "This is the user bio" -foobar_user.save() - -# GPG keys -gkey = new_user.gpgkeys.create({"key": GPG_KEY}) -assert len(new_user.gpgkeys.list()) == 1 -# Seems broken on the gitlab side -# gkey = new_user.gpgkeys.get(gkey.id) -gkey.delete() -assert len(new_user.gpgkeys.list()) == 0 - -# SSH keys -key = new_user.keys.create({"title": "testkey", "key": SSH_KEY}) -assert len(new_user.keys.list()) == 1 -key.delete() -assert len(new_user.keys.list()) == 0 - -# emails -email = new_user.emails.create({"email": "foo2@bar.com"}) -assert len(new_user.emails.list()) == 1 -email.delete() -assert len(new_user.emails.list()) == 0 - -# custom attributes -attrs = new_user.customattributes.list() -assert len(attrs) == 0 -attr = new_user.customattributes.set("key", "value1") -assert len(gl.users.list(custom_attributes={"key": "value1"})) == 1 -assert attr.key == "key" -assert attr.value == "value1" -assert len(new_user.customattributes.list()) == 1 -attr = new_user.customattributes.set("key", "value2") -attr = new_user.customattributes.get("key") -assert attr.value == "value2" -assert len(new_user.customattributes.list()) == 1 -attr.delete() -assert len(new_user.customattributes.list()) == 0 - -# impersonation tokens -user_token = new_user.impersonationtokens.create( - {"name": "token1", "scopes": ["api", "read_user"]} -) -l = new_user.impersonationtokens.list(state="active") -assert len(l) == 1 -user_token.delete() -l = new_user.impersonationtokens.list(state="active") -assert len(l) == 0 -l = new_user.impersonationtokens.list(state="inactive") -assert len(l) == 1 - -new_user.delete() -foobar_user.delete() -assert len(gl.users.list()) == 3 + len( - [u for u in gl.users.list() if u.username == "ghost"] -) - -# current user mail -mail = gl.user.emails.create({"email": "current@user.com"}) -assert len(gl.user.emails.list()) == 1 -mail.delete() -assert len(gl.user.emails.list()) == 0 - -# current user GPG keys -gkey = gl.user.gpgkeys.create({"key": GPG_KEY}) -assert len(gl.user.gpgkeys.list()) == 1 -# Seems broken on the gitlab side -gkey = gl.user.gpgkeys.get(gkey.id) -gkey.delete() -assert len(gl.user.gpgkeys.list()) == 0 - -# current user key -key = gl.user.keys.create({"title": "testkey", "key": SSH_KEY}) -assert len(gl.user.keys.list()) == 1 -key.delete() -assert len(gl.user.keys.list()) == 0 - -# templates -assert gl.dockerfiles.list() -dockerfile = gl.dockerfiles.get("Node") -assert dockerfile.content is not None - -assert gl.gitignores.list() -gitignore = gl.gitignores.get("Node") -assert gitignore.content is not None - -assert gl.gitlabciymls.list() -gitlabciyml = gl.gitlabciymls.get("Nodejs") -assert gitlabciyml.content is not None - -assert gl.licenses.list() -license = gl.licenses.get( - "bsd-2-clause", project="mytestproject", fullname="mytestfullname" -) -assert "mytestfullname" in license.content - -# groups -user1 = gl.users.create( - { - "email": "user1@test.com", - "username": "user1", - "name": "user1", - "password": "user1_pass", - } -) -user2 = gl.users.create( - { - "email": "user2@test.com", - "username": "user2", - "name": "user2", - "password": "user2_pass", - } -) -group1 = gl.groups.create({"name": "group1", "path": "group1"}) -group2 = gl.groups.create({"name": "group2", "path": "group2"}) - -p_id = gl.groups.list(search="group2")[0].id -group3 = gl.groups.create({"name": "group3", "path": "group3", "parent_id": p_id}) - -assert len(gl.groups.list()) == 3 -assert len(gl.groups.list(search="oup1")) == 1 -assert group3.parent_id == p_id -assert group2.subgroups.list()[0].id == group3.id - -group1.members.create({"access_level": gitlab.const.OWNER_ACCESS, "user_id": user1.id}) -group1.members.create({"access_level": gitlab.const.GUEST_ACCESS, "user_id": user2.id}) - -group2.members.create({"access_level": gitlab.const.OWNER_ACCESS, "user_id": user2.id}) - -# Administrator belongs to the groups -assert len(group1.members.list()) == 3 -assert len(group2.members.list()) == 2 - -group1.members.delete(user1.id) -assert len(group1.members.list()) == 2 -assert len(group1.members.all()) -member = group1.members.get(user2.id) -member.access_level = gitlab.const.OWNER_ACCESS -member.save() -member = group1.members.get(user2.id) -assert member.access_level == gitlab.const.OWNER_ACCESS - -group2.members.delete(gl.user.id) - -# group custom attributes -attrs = group2.customattributes.list() -assert len(attrs) == 0 -attr = group2.customattributes.set("key", "value1") -assert len(gl.groups.list(custom_attributes={"key": "value1"})) == 1 -assert attr.key == "key" -assert attr.value == "value1" -assert len(group2.customattributes.list()) == 1 -attr = group2.customattributes.set("key", "value2") -attr = group2.customattributes.get("key") -assert attr.value == "value2" -assert len(group2.customattributes.list()) == 1 -attr.delete() -assert len(group2.customattributes.list()) == 0 - -# group notification settings -settings = group2.notificationsettings.get() -settings.level = "disabled" -settings.save() -settings = group2.notificationsettings.get() -assert settings.level == "disabled" - -# group badges -badge_image = "http://example.com" -badge_link = "http://example/img.svg" -badge = group2.badges.create({"link_url": badge_link, "image_url": badge_image}) -assert len(group2.badges.list()) == 1 -badge.image_url = "http://another.example.com" -badge.save() -badge = group2.badges.get(badge.id) -assert badge.image_url == "http://another.example.com" -badge.delete() -assert len(group2.badges.list()) == 0 - -# group milestones -gm1 = group1.milestones.create({"title": "groupmilestone1"}) -assert len(group1.milestones.list()) == 1 -gm1.due_date = "2020-01-01T00:00:00Z" -gm1.save() -gm1.state_event = "close" -gm1.save() -gm1 = group1.milestones.get(gm1.id) -assert gm1.state == "closed" -assert len(gm1.issues()) == 0 -assert len(gm1.merge_requests()) == 0 - -# group variables -group1.variables.create({"key": "foo", "value": "bar"}) -g_v = group1.variables.get("foo") -assert g_v.value == "bar" -g_v.value = "baz" -g_v.save() -g_v = group1.variables.get("foo") -assert g_v.value == "baz" -assert len(group1.variables.list()) == 1 -g_v.delete() -assert len(group1.variables.list()) == 0 - -# group labels -# group1.labels.create({"name": "foo", "description": "bar", "color": "#112233"}) -# g_l = group1.labels.get("foo") -# assert g_l.description == "bar" -# g_l.description = "baz" -# g_l.save() -# g_l = group1.labels.get("foo") -# assert g_l.description == "baz" -# assert len(group1.labels.list()) == 1 -# g_l.delete() -# assert len(group1.labels.list()) == 0 - -# hooks -hook = gl.hooks.create({"url": "http://whatever.com"}) -assert len(gl.hooks.list()) == 1 -hook.delete() -assert len(gl.hooks.list()) == 0 - -# projects -admin_project = gl.projects.create({"name": "admin_project"}) -gr1_project = gl.projects.create({"name": "gr1_project", "namespace_id": group1.id}) -gr2_project = gl.projects.create({"name": "gr2_project", "namespace_id": group2.id}) -sudo_project = gl.projects.create({"name": "sudo_project"}, sudo=user1.name) - -assert len(gl.projects.list(owned=True)) == 2 -assert len(gl.projects.list(search="admin")) == 1 - -# test pagination -l1 = gl.projects.list(per_page=1, page=1) -l2 = gl.projects.list(per_page=1, page=2) -assert len(l1) == 1 -assert len(l2) == 1 -assert l1[0].id != l2[0].id - -# group custom attributes -attrs = admin_project.customattributes.list() -assert len(attrs) == 0 -attr = admin_project.customattributes.set("key", "value1") -assert len(gl.projects.list(custom_attributes={"key": "value1"})) == 1 -assert attr.key == "key" -assert attr.value == "value1" -assert len(admin_project.customattributes.list()) == 1 -attr = admin_project.customattributes.set("key", "value2") -attr = admin_project.customattributes.get("key") -assert attr.value == "value2" -assert len(admin_project.customattributes.list()) == 1 -attr.delete() -assert len(admin_project.customattributes.list()) == 0 - -# project pages domains -domain = admin_project.pagesdomains.create({"domain": "foo.domain.com"}) -assert len(admin_project.pagesdomains.list()) == 1 -assert len(gl.pagesdomains.list()) == 1 -domain = admin_project.pagesdomains.get("foo.domain.com") -assert domain.domain == "foo.domain.com" -domain.delete() -assert len(admin_project.pagesdomains.list()) == 0 - -# project content (files) -admin_project.files.create( - { - "file_path": "README", +async def main(): + # token authentication from config file + gl = gitlab.Gitlab.from_config(config_files=["/tmp/python-gitlab.cfg"]) + gl.enable_debug() + await gl.auth() + assert isinstance(gl.user, gitlab.v4.objects.CurrentUser) + + # markdown + html = await gl.markdown("foo") + assert "foo" in html + + success, errors = await gl.lint("Invalid") + assert success is False + assert errors + + # sidekiq + out = await gl.sidekiq.queue_metrics() + assert isinstance(out, dict) + assert "pages" in out["queues"] + out = await gl.sidekiq.process_metrics() + assert isinstance(out, dict) + assert "hostname" in out["processes"][0] + out = await gl.sidekiq.job_stats() + assert isinstance(out, dict) + assert "processed" in out["jobs"] + out = await gl.sidekiq.compound_metrics() + assert isinstance(out, dict) + assert "jobs" in out + assert "processes" in out + assert "queues" in out + + # settings + settings = await gl.settings.get() + settings.default_projects_limit = 42 + await settings.save() + settings = await gl.settings.get() + assert settings.default_projects_limit == 42 + + # users + new_user = await gl.users.create( + { + "email": "foo@bar.com", + "username": "foo", + "name": "foo", + "password": "foo_password", + "avatar": open(AVATAR_PATH, "rb"), + } + ) + avatar_url = new_user.avatar_url.replace("gitlab.test", "localhost:8080") + uploaded_avatar = httpx.get(avatar_url).content + assert uploaded_avatar == open(AVATAR_PATH, "rb").read() + users_list = await gl.users.list() + for user in users_list: + if user.username == "foo": + break + assert new_user.username == user.username + assert new_user.email == user.email + + await new_user.block() + await new_user.unblock() + + # user projects list + assert len(await new_user.projects.list()) == 0 + + # events list + await new_user.events.list() + + foobar_user = await gl.users.create( + { + "email": "foobar@example.com", + "username": "foobar", + "name": "Foo Bar", + "password": "foobar_password", + } + ) + + assert (await gl.users.list(search="foobar"))[0].id == foobar_user.id + expected = [new_user, foobar_user] + actual = list(await gl.users.list(search="foo")) + assert len(expected) == len(actual) + assert len(await gl.users.list(search="asdf")) == 0 + foobar_user.bio = "This is the user bio" + await foobar_user.save() + + # GPG keys + gkey = await new_user.gpgkeys.create({"key": GPG_KEY}) + assert len(await new_user.gpgkeys.list()) == 1 + # Seems broken on the gitlab side + # gkey = new_user.gpgkeys.get(gkey.id) + await gkey.delete() + assert len(await new_user.gpgkeys.list()) == 0 + + # SSH keys + key = await new_user.keys.create({"title": "testkey", "key": SSH_KEY}) + assert len(await new_user.keys.list()) == 1 + await key.delete() + assert len(await new_user.keys.list()) == 0 + + # emails + email = await new_user.emails.create({"email": "foo2@bar.com"}) + assert len(await new_user.emails.list()) == 1 + await email.delete() + assert len(await new_user.emails.list()) == 0 + + # custom attributes + attrs = await new_user.customattributes.list() + assert len(attrs) == 0 + attr = await new_user.customattributes.set("key", "value1") + assert len(await gl.users.list(custom_attributes={"key": "value1"})) == 1 + assert attr.key == "key" + assert attr.value == "value1" + assert len(await new_user.customattributes.list()) == 1 + attr = await new_user.customattributes.set("key", "value2") + attr = await new_user.customattributes.get("key") + assert attr.value == "value2" + assert len(await new_user.customattributes.list()) == 1 + await attr.delete() + assert len(await new_user.customattributes.list()) == 0 + + # impersonation tokens + user_token = await new_user.impersonationtokens.create( + {"name": "token1", "scopes": ["api", "read_user"]} + ) + l = await new_user.impersonationtokens.list(state="active") + assert len(l) == 1 + await user_token.delete() + l = await new_user.impersonationtokens.list(state="active") + assert len(l) == 0 + l = await new_user.impersonationtokens.list(state="inactive") + assert len(l) == 1 + + await new_user.delete() + await foobar_user.delete() + assert len(await gl.users.list()) == 3 + len( + [u for u in await gl.users.list() if u.username == "ghost"] + ) + + # current user mail + mail = await gl.user.emails.create({"email": "current@user.com"}) + assert len(await gl.user.emails.list()) == 1 + await mail.delete() + assert len(await gl.user.emails.list()) == 0 + + # current user GPG keys + gkey = await gl.user.gpgkeys.create({"key": GPG_KEY}) + assert len(await gl.user.gpgkeys.list()) == 1 + # Seems broken on the gitlab side + gkey = await gl.user.gpgkeys.get(gkey.id) + await gkey.delete() + assert len(await gl.user.gpgkeys.list()) == 0 + + # current user key + key = await gl.user.keys.create({"title": "testkey", "key": SSH_KEY}) + assert len(await gl.user.keys.list()) == 1 + await key.delete() + assert len(await gl.user.keys.list()) == 0 + + # templates + assert await gl.dockerfiles.list() + dockerfile = await gl.dockerfiles.get("Node") + assert dockerfile.content is not None + + assert await gl.gitignores.list() + gitignore = await gl.gitignores.get("Node") + assert gitignore.content is not None + + assert await gl.gitlabciymls.list() + gitlabciyml = await gl.gitlabciymls.get("Nodejs") + assert gitlabciyml.content is not None + + assert await gl.licenses.list() + license = await gl.licenses.get( + "bsd-2-clause", project="mytestproject", fullname="mytestfullname" + ) + assert "mytestfullname" in license.content + + # groups + user1 = await gl.users.create( + { + "email": "user1@test.com", + "username": "user1", + "name": "user1", + "password": "user1_pass", + } + ) + user2 = await gl.users.create( + { + "email": "user2@test.com", + "username": "user2", + "name": "user2", + "password": "user2_pass", + } + ) + group1 = await gl.groups.create({"name": "group1", "path": "group1"}) + group2 = await gl.groups.create({"name": "group2", "path": "group2"}) + + p_id = (await gl.groups.list(search="group2"))[0].id + group3 = await gl.groups.create( + {"name": "group3", "path": "group3", "parent_id": p_id} + ) + + assert len(await gl.groups.list()) == 3 + assert len(await gl.groups.list(search="oup1")) == 1 + assert group3.parent_id == p_id + assert (await group2.subgroups.list())[0].id == group3.id + + await group1.members.create( + {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user1.id} + ) + await group1.members.create( + {"access_level": gitlab.const.GUEST_ACCESS, "user_id": user2.id} + ) + + await group2.members.create( + {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user2.id} + ) + + # Administrator belongs to the groups + assert len(await group1.members.list()) == 3 + assert len(await group2.members.list()) == 2 + + await group1.members.delete(user1.id) + assert len(await group1.members.list()) == 2 + assert len(await group1.members.all()) + member = await group1.members.get(user2.id) + member.access_level = gitlab.const.OWNER_ACCESS + await member.save() + member = await group1.members.get(user2.id) + assert member.access_level == gitlab.const.OWNER_ACCESS + + await group2.members.delete(gl.user.id) + + # group custom attributes + attrs = await group2.customattributes.list() + assert len(attrs) == 0 + attr = await group2.customattributes.set("key", "value1") + assert len(await gl.groups.list(custom_attributes={"key": "value1"})) == 1 + assert attr.key == "key" + assert attr.value == "value1" + assert len(await group2.customattributes.list()) == 1 + attr = await group2.customattributes.set("key", "value2") + attr = await group2.customattributes.get("key") + assert attr.value == "value2" + assert len(await group2.customattributes.list()) == 1 + await attr.delete() + assert len(await group2.customattributes.list()) == 0 + + # group notification settings + settings = await group2.notificationsettings.get() + settings.level = "disabled" + await settings.save() + settings = await group2.notificationsettings.get() + assert settings.level == "disabled" + + # group badges + badge_image = "http://example.com" + badge_link = "http://example/img.svg" + badge = await group2.badges.create( + {"link_url": badge_link, "image_url": badge_image} + ) + assert len(await group2.badges.list()) == 1 + badge.image_url = "http://another.example.com" + await badge.save() + badge = await group2.badges.get(badge.id) + assert badge.image_url == "http://another.example.com" + await badge.delete() + assert len(await group2.badges.list()) == 0 + + # group milestones + gm1 = await group1.milestones.create({"title": "groupmilestone1"}) + assert len(await group1.milestones.list()) == 1 + gm1.due_date = "2020-01-01T00:00:00Z" + await gm1.save() + gm1.state_event = "close" + await gm1.save() + gm1 = await group1.milestones.get(gm1.id) + assert gm1.state == "closed" + assert len(await gm1.issues()) == 0 + assert len(await gm1.merge_requests()) == 0 + + # group variables + await group1.variables.create({"key": "foo", "value": "bar"}) + g_v = await group1.variables.get("foo") + assert g_v.value == "bar" + g_v.value = "baz" + await g_v.save() + g_v = await group1.variables.get("foo") + assert g_v.value == "baz" + assert len(await group1.variables.list()) == 1 + await g_v.delete() + assert len(await group1.variables.list()) == 0 + + # group labels + # group1.labels.create({"name": "foo", "description": "bar", "color": "#112233"}) + # g_l = group1.labels.get("foo") + # assert g_l.description == "bar" + # g_l.description = "baz" + # g_l.save() + # g_l = group1.labels.get("foo") + # assert g_l.description == "baz" + # assert len(group1.labels.list()) == 1 + # g_l.delete() + # assert len(group1.labels.list()) == 0 + + # hooks + hook = await gl.hooks.create({"url": "http://whatever.com"}) + assert len(await gl.hooks.list()) == 1 + await hook.delete() + assert len(await gl.hooks.list()) == 0 + + # projects + admin_project = await gl.projects.create({"name": "admin_project"}) + gr1_project = await gl.projects.create( + {"name": "gr1_project", "namespace_id": group1.id} + ) + gr2_project = await gl.projects.create( + {"name": "gr2_project", "namespace_id": group2.id} + ) + sudo_project = await gl.projects.create({"name": "sudo_project"}, sudo=user1.name) + + assert len(await gl.projects.list(owned=True)) == 2 + assert len(await gl.projects.list(search="admin")) == 1 + + # test pagination + l1 = await gl.projects.list(per_page=1, page=1) + l2 = await gl.projects.list(per_page=1, page=2) + assert len(l1) == 1 + assert len(l2) == 1 + assert l1[0].id != l2[0].id + + # group custom attributes + attrs = await admin_project.customattributes.list() + assert len(attrs) == 0 + attr = await admin_project.customattributes.set("key", "value1") + assert len(await gl.projects.list(custom_attributes={"key": "value1"})) == 1 + assert attr.key == "key" + assert attr.value == "value1" + assert len(await admin_project.customattributes.list()) == 1 + attr = await admin_project.customattributes.set("key", "value2") + attr = await admin_project.customattributes.get("key") + assert attr.value == "value2" + assert len(await admin_project.customattributes.list()) == 1 + await attr.delete() + assert len(await admin_project.customattributes.list()) == 0 + + # project pages domains + domain = await admin_project.pagesdomains.create({"domain": "foo.domain.com"}) + assert len(await admin_project.pagesdomains.list()) == 1 + assert len(await gl.pagesdomains.list()) == 1 + domain = await admin_project.pagesdomains.get("foo.domain.com") + assert domain.domain == "foo.domain.com" + await domain.delete() + assert len(await admin_project.pagesdomains.list()) == 0 + + # project content (files) + await admin_project.files.create( + { + "file_path": "README", + "branch": "master", + "content": "Initial content", + "commit_message": "Initial commit", + } + ) + readme = await admin_project.files.get(file_path="README", ref="master") + readme.content = base64.b64encode(b"Improved README").decode() + await asyncio.sleep(2) + await readme.save(branch="master", commit_message="new commit") + await readme.delete(commit_message="Removing README", branch="master") + + await admin_project.files.create( + { + "file_path": "README.rst", + "branch": "master", + "content": "Initial content", + "commit_message": "New commit", + } + ) + readme = await admin_project.files.get(file_path="README.rst", ref="master") + # The first decode() is the ProjectFile method, the second one is the bytes + # object method + assert readme.decode().decode() == "Initial content" + + blame = await admin_project.files.blame(file_path="README.rst", ref="master") + + data = { "branch": "master", - "content": "Initial content", - "commit_message": "Initial commit", + "commit_message": "blah blah blah", + "actions": [{"action": "create", "file_path": "blah", "content": "blah"}], } -) -readme = admin_project.files.get(file_path="README", ref="master") -readme.content = base64.b64encode(b"Improved README").decode() -time.sleep(2) -readme.save(branch="master", commit_message="new commit") -readme.delete(commit_message="Removing README", branch="master") - -admin_project.files.create( - { - "file_path": "README.rst", - "branch": "master", - "content": "Initial content", - "commit_message": "New commit", - } -) -readme = admin_project.files.get(file_path="README.rst", ref="master") -# The first decode() is the ProjectFile method, the second one is the bytes -# object method -assert readme.decode().decode() == "Initial content" - -blame = admin_project.files.blame(file_path="README.rst", ref="master") - -data = { - "branch": "master", - "commit_message": "blah blah blah", - "actions": [{"action": "create", "file_path": "blah", "content": "blah"}], -} -admin_project.commits.create(data) -assert "@@" in admin_project.commits.list()[0].diff()[0]["diff"] - -# commit status -commit = admin_project.commits.list()[0] -# size = len(commit.statuses.list()) -# status = commit.statuses.create({"state": "success", "sha": commit.id}) -# assert len(commit.statuses.list()) == size + 1 - -# assert commit.refs() -# assert commit.merge_requests() - -# commit comment -commit.comments.create({"note": "This is a commit comment"}) -# assert len(commit.comments.list()) == 1 - -# commit discussion -count = len(commit.discussions.list()) -discussion = commit.discussions.create({"body": "Discussion body"}) -# assert len(commit.discussions.list()) == (count + 1) -d_note = discussion.notes.create({"body": "first note"}) -d_note_from_get = discussion.notes.get(d_note.id) -d_note_from_get.body = "updated body" -d_note_from_get.save() -discussion = commit.discussions.get(discussion.id) -# assert discussion.attributes["notes"][-1]["body"] == "updated body" -d_note_from_get.delete() -discussion = commit.discussions.get(discussion.id) -# assert len(discussion.attributes["notes"]) == 1 - -# housekeeping -admin_project.housekeeping() - -# repository -tree = admin_project.repository_tree() -assert len(tree) != 0 -assert tree[0]["name"] == "README.rst" -blob_id = tree[0]["id"] -blob = admin_project.repository_raw_blob(blob_id) -assert blob.decode() == "Initial content" -archive1 = admin_project.repository_archive() -archive2 = admin_project.repository_archive("master") -assert archive1 == archive2 -snapshot = admin_project.snapshot() - -# project file uploads -filename = "test.txt" -file_contents = "testing contents" -uploaded_file = admin_project.upload(filename, file_contents) -assert uploaded_file["alt"] == filename -assert uploaded_file["url"].startswith("/uploads/") -assert uploaded_file["url"].endswith("/" + filename) -assert uploaded_file["markdown"] == "[{}]({})".format( - uploaded_file["alt"], uploaded_file["url"] -) + await admin_project.commits.create(data) + assert "@@" in (await (await admin_project.commits.list())[0].diff())[0]["diff"] + + # commit status + commit = (await admin_project.commits.list())[0] + # size = len(commit.statuses.list()) + # status = commit.statuses.create({"state": "success", "sha": commit.id}) + # assert len(commit.statuses.list()) == size + 1 + + # assert commit.refs() + # assert commit.merge_requests() + + # commit comment + await commit.comments.create({"note": "This is a commit comment"}) + # assert len(commit.comments.list()) == 1 + + # commit discussion + count = len(await commit.discussions.list()) + discussion = await commit.discussions.create({"body": "Discussion body"}) + # assert len(commit.discussions.list()) == (count + 1) + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await commit.discussions.get(discussion.id) + # assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await commit.discussions.get(discussion.id) + # assert len(discussion.attributes["notes"]) == 1 + + # housekeeping + await admin_project.housekeeping() + + # repository + tree = await admin_project.repository_tree() + assert len(tree) != 0 + assert tree[0]["name"] == "README.rst" + blob_id = tree[0]["id"] + blob = await admin_project.repository_raw_blob(blob_id) + assert blob.decode() == "Initial content" + archive1 = await admin_project.repository_archive() + archive2 = await admin_project.repository_archive("master") + assert archive1 == archive2 + snapshot = await admin_project.snapshot() + + # project file uploads + filename = "test.txt" + file_contents = "testing contents" + uploaded_file = await admin_project.upload(filename, file_contents) + assert uploaded_file["alt"] == filename + assert uploaded_file["url"].startswith("/uploads/") + assert uploaded_file["url"].endswith("/" + filename) + assert uploaded_file["markdown"] == "[{}]({})".format( + uploaded_file["alt"], uploaded_file["url"] + ) + + # environments + await admin_project.environments.create( + {"name": "env1", "external_url": "http://fake.env/whatever"} + ) + envs = await admin_project.environments.list() + assert len(envs) == 1 + env = envs[0] + env.external_url = "http://new.env/whatever" + await env.save() + env = (await admin_project.environments.list())[0] + assert env.external_url == "http://new.env/whatever" + await env.stop() + await env.delete() + assert len(await admin_project.environments.list()) == 0 + + # Project clusters + await admin_project.clusters.create( + { + "name": "cluster1", + "platform_kubernetes_attributes": { + "api_url": "http://url", + "token": "tokenval", + }, + } + ) + clusters = await admin_project.clusters.list() + assert len(clusters) == 1 + cluster = clusters[0] + cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} + await cluster.save() + cluster = (await admin_project.clusters.list())[0] + assert cluster.platform_kubernetes["api_url"] == "http://newurl" + await cluster.delete() + assert len(await admin_project.clusters.list()) == 0 + + # Group clusters + await group1.clusters.create( + { + "name": "cluster1", + "platform_kubernetes_attributes": { + "api_url": "http://url", + "token": "tokenval", + }, + } + ) + clusters = await group1.clusters.list() + assert len(clusters) == 1 + cluster = clusters[0] + cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} + await cluster.save() + cluster = (await group1.clusters.list())[0] + assert cluster.platform_kubernetes["api_url"] == "http://newurl" + await cluster.delete() + assert len(await group1.clusters.list()) == 0 + + # project events + await admin_project.events.list() + + # forks + fork = await admin_project.forks.create({"namespace": user1.username}) + p = await gl.projects.get(fork.id) + assert p.forked_from_project["id"] == admin_project.id + + forks = await admin_project.forks.list() + assert fork.id in map(lambda p: p.id, forks) + + # project hooks + hook = await admin_project.hooks.create({"url": "http://hook.url"}) + assert len(await admin_project.hooks.list()) == 1 + hook.note_events = True + await hook.save() + hook = await admin_project.hooks.get(hook.id) + assert hook.note_events is True + await hook.delete() + + # deploy keys + deploy_key = await admin_project.keys.create( + {"title": "foo@bar", "key": DEPLOY_KEY} + ) + project_keys = list(await admin_project.keys.list()) + assert len(project_keys) == 1 + + await sudo_project.keys.enable(deploy_key.id) + assert len(await sudo_project.keys.list()) == 1 + await sudo_project.keys.delete(deploy_key.id) + assert len(await sudo_project.keys.list()) == 0 + + # labels + # label1 = admin_project.labels.create({"name": "label1", "color": "#778899"}) + # label1 = admin_project.labels.list()[0] + # assert len(admin_project.labels.list()) == 1 + # label1.new_name = "label1updated" + # label1.save() + # assert label1.name == "label1updated" + # label1.subscribe() + # assert label1.subscribed == True + # label1.unsubscribe() + # assert label1.subscribed == False + # label1.delete() + + # milestones + m1 = await admin_project.milestones.create({"title": "milestone1"}) + assert len(await admin_project.milestones.list()) == 1 + m1.due_date = "2020-01-01T00:00:00Z" + await m1.save() + m1.state_event = "close" + await m1.save() + m1 = await admin_project.milestones.get(m1.id) + assert m1.state == "closed" + assert len(await m1.issues()) == 0 + assert len(await m1.merge_requests()) == 0 + + # issues + issue1 = await admin_project.issues.create( + {"title": "my issue 1", "milestone_id": m1.id} + ) + issue2 = await admin_project.issues.create({"title": "my issue 2"}) + issue3 = await admin_project.issues.create({"title": "my issue 3"}) + assert len(await admin_project.issues.list()) == 3 + issue3.state_event = "close" + await issue3.save() + assert len(await admin_project.issues.list(state="closed")) == 1 + assert len(await admin_project.issues.list(state="opened")) == 2 + assert len(await admin_project.issues.list(milestone="milestone1")) == 1 + assert (await (await m1.issues()).next()).title == "my issue 1" + size = len(await issue1.notes.list()) + note = await issue1.notes.create({"body": "This is an issue note"}) + assert len(await issue1.notes.list()) == size + 1 + emoji = await note.awardemojis.create({"name": "tractor"}) + assert len(await note.awardemojis.list()) == 1 + await emoji.delete() + assert len(await note.awardemojis.list()) == 0 + await note.delete() + assert len(await issue1.notes.list()) == size + assert isinstance(await issue1.user_agent_detail(), dict) + + assert (await issue1.user_agent_detail())["user_agent"] + assert await issue1.participants() + assert type(await issue1.closed_by()) == list + assert type(await issue1.related_merge_requests()) == list + + # issues labels and events + label2 = await admin_project.labels.create({"name": "label2", "color": "#aabbcc"}) + issue1.labels = ["label2"] + await issue1.save() + events = await issue1.resourcelabelevents.list() + assert events + event = await issue1.resourcelabelevents.get(events[0].id) + assert event + + size = len(await issue1.discussions.list()) + discussion = await issue1.discussions.create({"body": "Discussion body"}) + assert len(await issue1.discussions.list()) == size + 1 + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await issue1.discussions.get(discussion.id) + assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await issue1.discussions.get(discussion.id) + assert len(discussion.attributes["notes"]) == 1 + + # tags + tag1 = await admin_project.tags.create({"tag_name": "v1.0", "ref": "master"}) + assert len(await admin_project.tags.list()) == 1 + await tag1.set_release_description("Description 1") + await tag1.set_release_description("Description 2") + assert tag1.release["description"] == "Description 2" + await tag1.delete() + + # project snippet + admin_project.snippets_enabled = True + await admin_project.save() + snippet = await admin_project.snippets.create( + { + "title": "snip1", + "file_name": "foo.py", + "content": "initial content", + "visibility": gitlab.v4.objects.VISIBILITY_PRIVATE, + } + ) + + assert (await snippet.user_agent_detail())["user_agent"] + + size = len(await snippet.discussions.list()) + discussion = await snippet.discussions.create({"body": "Discussion body"}) + assert len(await snippet.discussions.list()) == size + 1 + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await snippet.discussions.get(discussion.id) + assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await snippet.discussions.get(discussion.id) + assert len(discussion.attributes["notes"]) == 1 + + snippet.file_name = "bar.py" + await snippet.save() + snippet = await admin_project.snippets.get(snippet.id) + assert (await snippet.content()).decode() == "initial content" + assert snippet.file_name == "bar.py" + size = len(await admin_project.snippets.list()) + await snippet.delete() + assert len(await admin_project.snippets.list()) == (size - 1) + + # triggers + tr1 = await admin_project.triggers.create({"description": "trigger1"}) + assert len(await admin_project.triggers.list()) == 1 + await tr1.delete() + + # variables + v1 = await admin_project.variables.create({"key": "key1", "value": "value1"}) + assert len(await admin_project.variables.list()) == 1 + v1.value = "new_value1" + await v1.save() + v1 = await admin_project.variables.get(v1.key) + assert v1.value == "new_value1" + await v1.delete() + + # branches and merges + to_merge = await admin_project.branches.create( + {"branch": "branch1", "ref": "master"} + ) + await admin_project.files.create( + { + "file_path": "README2.rst", + "branch": "branch1", + "content": "Initial content", + "commit_message": "New commit in new branch", + } + ) + mr = await admin_project.mergerequests.create( + {"source_branch": "branch1", "target_branch": "master", "title": "MR readme2"} + ) + + # discussion + size = len(await mr.discussions.list()) + discussion = await mr.discussions.create({"body": "Discussion body"}) + assert len(await mr.discussions.list()) == size + 1 + d_note = await discussion.notes.create({"body": "first note"}) + d_note_from_get = await discussion.notes.get(d_note.id) + d_note_from_get.body = "updated body" + await d_note_from_get.save() + discussion = await mr.discussions.get(discussion.id) + assert discussion.attributes["notes"][-1]["body"] == "updated body" + await d_note_from_get.delete() + discussion = await mr.discussions.get(discussion.id) + assert len(discussion.attributes["notes"]) == 1 + + # mr labels and events + mr.labels = ["label2"] + await mr.save() + events = await mr.resourcelabelevents.list() + assert events + event = await mr.resourcelabelevents.get(events[0].id) + assert event + + # rebasing + assert await mr.rebase() + + # basic testing: only make sure that the methods exist + await mr.commits() + await mr.changes() + assert await mr.participants() + + await mr.merge() + await admin_project.branches.delete("branch1") -# environments -admin_project.environments.create( - {"name": "env1", "external_url": "http://fake.env/whatever"} -) -envs = admin_project.environments.list() -assert len(envs) == 1 -env = envs[0] -env.external_url = "http://new.env/whatever" -env.save() -env = admin_project.environments.list()[0] -assert env.external_url == "http://new.env/whatever" -env.stop() -env.delete() -assert len(admin_project.environments.list()) == 0 - -# Project clusters -admin_project.clusters.create( - { - "name": "cluster1", - "platform_kubernetes_attributes": { - "api_url": "http://url", - "token": "tokenval", - }, - } -) -clusters = admin_project.clusters.list() -assert len(clusters) == 1 -cluster = clusters[0] -cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} -cluster.save() -cluster = admin_project.clusters.list()[0] -assert cluster.platform_kubernetes["api_url"] == "http://newurl" -cluster.delete() -assert len(admin_project.clusters.list()) == 0 - -# Group clusters -group1.clusters.create( - { - "name": "cluster1", - "platform_kubernetes_attributes": { - "api_url": "http://url", - "token": "tokenval", - }, - } -) -clusters = group1.clusters.list() -assert len(clusters) == 1 -cluster = clusters[0] -cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} -cluster.save() -cluster = group1.clusters.list()[0] -assert cluster.platform_kubernetes["api_url"] == "http://newurl" -cluster.delete() -assert len(group1.clusters.list()) == 0 - -# project events -admin_project.events.list() - -# forks -fork = admin_project.forks.create({"namespace": user1.username}) -p = gl.projects.get(fork.id) -assert p.forked_from_project["id"] == admin_project.id - -forks = admin_project.forks.list() -assert fork.id in map(lambda p: p.id, forks) - -# project hooks -hook = admin_project.hooks.create({"url": "http://hook.url"}) -assert len(admin_project.hooks.list()) == 1 -hook.note_events = True -hook.save() -hook = admin_project.hooks.get(hook.id) -assert hook.note_events is True -hook.delete() - -# deploy keys -deploy_key = admin_project.keys.create({"title": "foo@bar", "key": DEPLOY_KEY}) -project_keys = list(admin_project.keys.list()) -assert len(project_keys) == 1 - -sudo_project.keys.enable(deploy_key.id) -assert len(sudo_project.keys.list()) == 1 -sudo_project.keys.delete(deploy_key.id) -assert len(sudo_project.keys.list()) == 0 - -# labels -# label1 = admin_project.labels.create({"name": "label1", "color": "#778899"}) -# label1 = admin_project.labels.list()[0] -# assert len(admin_project.labels.list()) == 1 -# label1.new_name = "label1updated" -# label1.save() -# assert label1.name == "label1updated" -# label1.subscribe() -# assert label1.subscribed == True -# label1.unsubscribe() -# assert label1.subscribed == False -# label1.delete() - -# milestones -m1 = admin_project.milestones.create({"title": "milestone1"}) -assert len(admin_project.milestones.list()) == 1 -m1.due_date = "2020-01-01T00:00:00Z" -m1.save() -m1.state_event = "close" -m1.save() -m1 = admin_project.milestones.get(m1.id) -assert m1.state == "closed" -assert len(m1.issues()) == 0 -assert len(m1.merge_requests()) == 0 - -# issues -issue1 = admin_project.issues.create({"title": "my issue 1", "milestone_id": m1.id}) -issue2 = admin_project.issues.create({"title": "my issue 2"}) -issue3 = admin_project.issues.create({"title": "my issue 3"}) -assert len(admin_project.issues.list()) == 3 -issue3.state_event = "close" -issue3.save() -assert len(admin_project.issues.list(state="closed")) == 1 -assert len(admin_project.issues.list(state="opened")) == 2 -assert len(admin_project.issues.list(milestone="milestone1")) == 1 -assert m1.issues().next().title == "my issue 1" -size = len(issue1.notes.list()) -note = issue1.notes.create({"body": "This is an issue note"}) -assert len(issue1.notes.list()) == size + 1 -emoji = note.awardemojis.create({"name": "tractor"}) -assert len(note.awardemojis.list()) == 1 -emoji.delete() -assert len(note.awardemojis.list()) == 0 -note.delete() -assert len(issue1.notes.list()) == size -assert isinstance(issue1.user_agent_detail(), dict) - -assert issue1.user_agent_detail()["user_agent"] -assert issue1.participants() -assert type(issue1.closed_by()) == list -assert type(issue1.related_merge_requests()) == list - -# issues labels and events -label2 = admin_project.labels.create({"name": "label2", "color": "#aabbcc"}) -issue1.labels = ["label2"] -issue1.save() -events = issue1.resourcelabelevents.list() -assert events -event = issue1.resourcelabelevents.get(events[0].id) -assert event - - -size = len(issue1.discussions.list()) -discussion = issue1.discussions.create({"body": "Discussion body"}) -assert len(issue1.discussions.list()) == size + 1 -d_note = discussion.notes.create({"body": "first note"}) -d_note_from_get = discussion.notes.get(d_note.id) -d_note_from_get.body = "updated body" -d_note_from_get.save() -discussion = issue1.discussions.get(discussion.id) -assert discussion.attributes["notes"][-1]["body"] == "updated body" -d_note_from_get.delete() -discussion = issue1.discussions.get(discussion.id) -assert len(discussion.attributes["notes"]) == 1 - -# tags -tag1 = admin_project.tags.create({"tag_name": "v1.0", "ref": "master"}) -assert len(admin_project.tags.list()) == 1 -tag1.set_release_description("Description 1") -tag1.set_release_description("Description 2") -assert tag1.release["description"] == "Description 2" -tag1.delete() - -# project snippet -admin_project.snippets_enabled = True -admin_project.save() -snippet = admin_project.snippets.create( - { - "title": "snip1", - "file_name": "foo.py", - "content": "initial content", - "visibility": gitlab.v4.objects.VISIBILITY_PRIVATE, - } -) - -assert snippet.user_agent_detail()["user_agent"] - -size = len(snippet.discussions.list()) -discussion = snippet.discussions.create({"body": "Discussion body"}) -assert len(snippet.discussions.list()) == size + 1 -d_note = discussion.notes.create({"body": "first note"}) -d_note_from_get = discussion.notes.get(d_note.id) -d_note_from_get.body = "updated body" -d_note_from_get.save() -discussion = snippet.discussions.get(discussion.id) -assert discussion.attributes["notes"][-1]["body"] == "updated body" -d_note_from_get.delete() -discussion = snippet.discussions.get(discussion.id) -assert len(discussion.attributes["notes"]) == 1 - -snippet.file_name = "bar.py" -snippet.save() -snippet = admin_project.snippets.get(snippet.id) -assert snippet.content().decode() == "initial content" -assert snippet.file_name == "bar.py" -size = len(admin_project.snippets.list()) -snippet.delete() -assert len(admin_project.snippets.list()) == (size - 1) - -# triggers -tr1 = admin_project.triggers.create({"description": "trigger1"}) -assert len(admin_project.triggers.list()) == 1 -tr1.delete() - -# variables -v1 = admin_project.variables.create({"key": "key1", "value": "value1"}) -assert len(admin_project.variables.list()) == 1 -v1.value = "new_value1" -v1.save() -v1 = admin_project.variables.get(v1.key) -assert v1.value == "new_value1" -v1.delete() - -# branches and merges -to_merge = admin_project.branches.create({"branch": "branch1", "ref": "master"}) -admin_project.files.create( - { - "file_path": "README2.rst", - "branch": "branch1", - "content": "Initial content", - "commit_message": "New commit in new branch", - } -) -mr = admin_project.mergerequests.create( - {"source_branch": "branch1", "target_branch": "master", "title": "MR readme2"} -) - -# discussion -size = len(mr.discussions.list()) -discussion = mr.discussions.create({"body": "Discussion body"}) -assert len(mr.discussions.list()) == size + 1 -d_note = discussion.notes.create({"body": "first note"}) -d_note_from_get = discussion.notes.get(d_note.id) -d_note_from_get.body = "updated body" -d_note_from_get.save() -discussion = mr.discussions.get(discussion.id) -assert discussion.attributes["notes"][-1]["body"] == "updated body" -d_note_from_get.delete() -discussion = mr.discussions.get(discussion.id) -assert len(discussion.attributes["notes"]) == 1 - -# mr labels and events -mr.labels = ["label2"] -mr.save() -events = mr.resourcelabelevents.list() -assert events -event = mr.resourcelabelevents.get(events[0].id) -assert event - -# rebasing -assert mr.rebase() - -# basic testing: only make sure that the methods exist -mr.commits() -mr.changes() -assert mr.participants() - -mr.merge() -admin_project.branches.delete("branch1") - -try: - mr.merge() -except gitlab.GitlabMRClosedError: - pass - -# protected branches -p_b = admin_project.protectedbranches.create({"name": "*-stable"}) -assert p_b.name == "*-stable" -p_b = admin_project.protectedbranches.get("*-stable") -# master is protected by default when a branch has been created -assert len(admin_project.protectedbranches.list()) == 2 -admin_project.protectedbranches.delete("master") -p_b.delete() -assert len(admin_project.protectedbranches.list()) == 0 - -# stars -admin_project.star() -assert admin_project.star_count == 1 -admin_project.unstar() -assert admin_project.star_count == 0 - -# project boards -# boards = admin_project.boards.list() -# assert(len(boards)) -# board = boards[0] -# lists = board.lists.list() -# begin_size = len(lists) -# last_list = lists[-1] -# last_list.position = 0 -# last_list.save() -# last_list.delete() -# lists = board.lists.list() -# assert(len(lists) == begin_size - 1) - -# project badges -badge_image = "http://example.com" -badge_link = "http://example/img.svg" -badge = admin_project.badges.create({"link_url": badge_link, "image_url": badge_image}) -assert len(admin_project.badges.list()) == 1 -badge.image_url = "http://another.example.com" -badge.save() -badge = admin_project.badges.get(badge.id) -assert badge.image_url == "http://another.example.com" -badge.delete() -assert len(admin_project.badges.list()) == 0 - -# project wiki -wiki_content = "Wiki page content" -wp = admin_project.wikis.create({"title": "wikipage", "content": wiki_content}) -assert len(admin_project.wikis.list()) == 1 -wp = admin_project.wikis.get(wp.slug) -assert wp.content == wiki_content -# update and delete seem broken -# wp.content = 'new content' -# wp.save() -# wp.delete() -# assert(len(admin_project.wikis.list()) == 0) - -# namespaces -ns = gl.namespaces.list(all=True) -assert len(ns) != 0 -ns = gl.namespaces.list(search="root", all=True)[0] -assert ns.kind == "user" - -# features -# Disabled as this fails with GitLab 11.11 -# feat = gl.features.set("foo", 30) -# assert feat.name == "foo" -# assert len(gl.features.list()) == 1 -# feat.delete() -# assert len(gl.features.list()) == 0 - -# broadcast messages -msg = gl.broadcastmessages.create({"message": "this is the message"}) -msg.color = "#444444" -msg.save() -msg_id = msg.id -msg = gl.broadcastmessages.list(all=True)[0] -assert msg.color == "#444444" -msg = gl.broadcastmessages.get(msg_id) -assert msg.color == "#444444" -msg.delete() -assert len(gl.broadcastmessages.list()) == 0 - -# notification settings -settings = gl.notificationsettings.get() -settings.level = gitlab.NOTIFICATION_LEVEL_WATCH -settings.save() -settings = gl.notificationsettings.get() -assert settings.level == gitlab.NOTIFICATION_LEVEL_WATCH - -# services -service = admin_project.services.get("asana") -service.api_key = "whatever" -service.save() -service = admin_project.services.get("asana") -assert service.active == True -service.delete() -service = admin_project.services.get("asana") -assert service.active == False - -# snippets -snippets = gl.snippets.list(all=True) -assert len(snippets) == 0 -snippet = gl.snippets.create( - {"title": "snippet1", "file_name": "snippet1.py", "content": "import gitlab"} -) -snippet = gl.snippets.get(snippet.id) -snippet.title = "updated_title" -snippet.save() -snippet = gl.snippets.get(snippet.id) -assert snippet.title == "updated_title" -content = snippet.content() -assert content.decode() == "import gitlab" - -assert snippet.user_agent_detail()["user_agent"] - -snippet.delete() -snippets = gl.snippets.list(all=True) -assert len(snippets) == 0 - -# user activities -gl.user_activities.list(query_parameters={"from": "2019-01-01"}) - -# events -gl.events.list() - -# rate limit -settings = gl.settings.get() -settings.throttle_authenticated_api_enabled = True -settings.throttle_authenticated_api_requests_per_period = 1 -settings.throttle_authenticated_api_period_in_seconds = 3 -settings.save() -projects = list() -for i in range(0, 20): - projects.append(gl.projects.create({"name": str(i) + "ok"})) - -error_message = None -for i in range(20, 40): try: - projects.append( - gl.projects.create({"name": str(i) + "shouldfail"}, obey_rate_limit=False) - ) - except gitlab.GitlabCreateError as e: - error_message = e.error_message - break -assert "Retry later" in error_message -settings.throttle_authenticated_api_enabled = False -settings.save() -[current_project.delete() for current_project in projects] - -# project import/export -ex = admin_project.exports.create({}) -ex.refresh() -count = 0 -while ex.export_status != "finished": - time.sleep(1) - ex.refresh() - count += 1 - if count == 10: - raise Exception("Project export taking too much time") -with open("/tmp/gitlab-export.tgz", "wb") as f: - ex.download(streamed=True, action=f.write) - -output = gl.projects.import_project( - open("/tmp/gitlab-export.tgz", "rb"), "imported_project" -) -project_import = gl.projects.get(output["id"], lazy=True).imports.get() -count = 0 -while project_import.import_status != "finished": - time.sleep(1) - project_import.refresh() - count += 1 - if count == 10: - raise Exception("Project import taking too much time") - -# project releases -release_test_project = gl.projects.create( - {"name": "release-test-project", "initialize_with_readme": True} -) -release_name = "Demo Release" -release_tag_name = "v1.2.3" -release_description = "release notes go here" -release_test_project.releases.create( - { - "name": release_name, - "tag_name": release_tag_name, - "description": release_description, - "ref": "master", - } -) -assert len(release_test_project.releases.list()) == 1 - -# get single release -retrieved_project = release_test_project.releases.get(release_tag_name) -assert retrieved_project.name == release_name -assert retrieved_project.tag_name == release_tag_name -assert retrieved_project.description == release_description - -# delete release -release_test_project.releases.delete(release_tag_name) -assert len(release_test_project.releases.list()) == 0 -release_test_project.delete() - -# status -message = "Test" -emoji = "thumbsup" -status = gl.user.status.get() -status.message = message -status.emoji = emoji -status.save() -new_status = gl.user.status.get() -assert new_status.message == message -assert new_status.emoji == emoji + await mr.merge() + except gitlab.GitlabMRClosedError: + pass + + # protected branches + p_b = await admin_project.protectedbranches.create({"name": "*-stable"}) + assert p_b.name == "*-stable" + p_b = await admin_project.protectedbranches.get("*-stable") + # master is protected by default when a branch has been created + assert len(await admin_project.protectedbranches.list()) == 2 + await admin_project.protectedbranches.delete("master") + await p_b.delete() + assert len(await admin_project.protectedbranches.list()) == 0 + + # stars + await admin_project.star() + assert admin_project.star_count == 1 + await admin_project.unstar() + assert admin_project.star_count == 0 + + # project boards + # boards = admin_project.boards.list() + # assert(len(boards)) + # board = boards[0] + # lists = board.lists.list() + # begin_size = len(lists) + # last_list = lists[-1] + # last_list.position = 0 + # last_list.save() + # last_list.delete() + # lists = board.lists.list() + # assert(len(lists) == begin_size - 1) + + # project badges + badge_image = "http://example.com" + badge_link = "http://example/img.svg" + badge = await admin_project.badges.create( + {"link_url": badge_link, "image_url": badge_image} + ) + assert len(await admin_project.badges.list()) == 1 + badge.image_url = "http://another.example.com" + await badge.save() + badge = await admin_project.badges.get(badge.id) + assert badge.image_url == "http://another.example.com" + await badge.delete() + assert len(await admin_project.badges.list()) == 0 + + # project wiki + wiki_content = "Wiki page content" + wp = await admin_project.wikis.create( + {"title": "wikipage", "content": wiki_content} + ) + assert len(await admin_project.wikis.list()) == 1 + wp = await admin_project.wikis.get(wp.slug) + assert wp.content == wiki_content + # update and delete seem broken + # wp.content = 'new content' + # wp.save() + # wp.delete() + # assert(len(admin_project.wikis.list()) == 0) + + # namespaces + ns = await gl.namespaces.list(all=True) + assert len(ns) != 0 + ns = (await gl.namespaces.list(search="root", all=True))[0] + assert ns.kind == "user" + + # features + # Disabled as this fails with GitLab 11.11 + # feat = gl.features.set("foo", 30) + # assert feat.name == "foo" + # assert len(gl.features.list()) == 1 + # feat.delete() + # assert len(gl.features.list()) == 0 + + # broadcast messages + msg = await gl.broadcastmessages.create({"message": "this is the message"}) + msg.color = "#444444" + await msg.save() + msg_id = msg.id + msg = (await gl.broadcastmessages.list(all=True))[0] + assert msg.color == "#444444" + msg = await gl.broadcastmessages.get(msg_id) + assert msg.color == "#444444" + await msg.delete() + assert len(await gl.broadcastmessages.list()) == 0 + + # notification settings + settings = await gl.notificationsettings.get() + settings.level = gitlab.NOTIFICATION_LEVEL_WATCH + await settings.save() + settings = await gl.notificationsettings.get() + assert settings.level == gitlab.NOTIFICATION_LEVEL_WATCH + + # services + service = await admin_project.services.get("asana") + service.api_key = "whatever" + await service.save() + service = await admin_project.services.get("asana") + assert service.active == True + await service.delete() + service = await admin_project.services.get("asana") + assert service.active == False + + # snippets + snippets = await gl.snippets.list(all=True) + assert len(snippets) == 0 + snippet = await gl.snippets.create( + {"title": "snippet1", "file_name": "snippet1.py", "content": "import gitlab"} + ) + snippet = await gl.snippets.get(snippet.id) + snippet.title = "updated_title" + await snippet.save() + snippet = await gl.snippets.get(snippet.id) + assert snippet.title == "updated_title" + content = await snippet.content() + assert content.decode() == "import gitlab" + + assert (await snippet.user_agent_detail())["user_agent"] + + await snippet.delete() + snippets = await gl.snippets.list(all=True) + assert len(snippets) == 0 + + # user activities + await gl.user_activities.list(query_parameters={"from": "2019-01-01"}) + + # events + await gl.events.list() + + # rate limit + settings = await gl.settings.get() + settings.throttle_authenticated_api_enabled = True + settings.throttle_authenticated_api_requests_per_period = 1 + settings.throttle_authenticated_api_period_in_seconds = 3 + await settings.save() + projects = list() + for i in range(0, 20): + projects.append(await gl.projects.create({"name": str(i) + "ok"})) + + error_message = None + for i in range(20, 40): + try: + projects.append( + await gl.projects.create( + {"name": str(i) + "shouldfail"}, obey_rate_limit=False + ) + ) + except gitlab.GitlabCreateError as e: + error_message = e.error_message + break + assert "Retry later" in error_message + settings.throttle_authenticated_api_enabled = False + await settings.save() + [await current_project.delete() for current_project in projects] + + # project import/export + ex = await admin_project.exports.create({}) + await ex.refresh() + count = 0 + while ex.export_status != "finished": + await asyncio.sleep(1) + await ex.refresh() + count += 1 + if count == 10: + raise Exception("Project export taking too much time") + with open("/tmp/gitlab-export.tgz", "wb") as f: + await ex.download(streamed=True, action=f.write) + + output = await gl.projects.import_project( + open("/tmp/gitlab-export.tgz", "rb"), "imported_project" + ) + project_import = await ( + await gl.projects.get(output["id"], lazy=True) + ).imports.get() + count = 0 + while project_import.import_status != "finished": + await asyncio.sleep(1) + await project_import.refresh() + count += 1 + if count == 10: + raise Exception("Project import taking too much time") + + # project releases + release_test_project = await gl.projects.create( + {"name": "release-test-project", "initialize_with_readme": True} + ) + release_name = "Demo Release" + release_tag_name = "v1.2.3" + release_description = "release notes go here" + await release_test_project.releases.create( + { + "name": release_name, + "tag_name": release_tag_name, + "description": release_description, + "ref": "master", + } + ) + assert len(await release_test_project.releases.list()) == 1 + + # get single release + retrieved_project = await release_test_project.releases.get(release_tag_name) + assert retrieved_project.name == release_name + assert retrieved_project.tag_name == release_tag_name + assert retrieved_project.description == release_description + + # delete release + await release_test_project.releases.delete(release_tag_name) + assert len(await release_test_project.releases.list()) == 0 + await release_test_project.delete() + + # status + message = "Test" + emoji = "thumbsup" + status = await gl.user.status.get() + status.message = message + status.emoji = emoji + await status.save() + new_status = await gl.user.status.get() + assert new_status.message == message + assert new_status.emoji == emoji + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/tox.ini b/tox.ini index 0aa43f09e..70ec5921a 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ install_command = pip install {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = - python setup.py testr --testr-args='{posargs}' + pytest [testenv:pep8] commands =