From f2808cc33331ee40fbb2ca3788a088035d0f55bf Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Thu, 30 Mar 2017 23:39:15 +0300 Subject: [PATCH 01/33] Specify organization when creating a repo. --- gogs_client/entities.py | 2 +- gogs_client/interface.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gogs_client/entities.py b/gogs_client/entities.py index a117a0f..956a799 100644 --- a/gogs_client/entities.py +++ b/gogs_client/entities.py @@ -239,4 +239,4 @@ def pull(self): :rtype: bool """ - return self._pull + return self._pull \ No newline at end of file diff --git a/gogs_client/interface.py b/gogs_client/interface.py index 95ac587..17892d5 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -111,7 +111,8 @@ def ensure_token(self, auth, name, username=None): return self.create_token(auth, name, username) def create_repo(self, auth, name, description=None, private=False, auto_init=False, - gitignore_templates=None, license_template=None, readme_template=None): + gitignore_templates=None, license_template=None, readme_template=None, + organization=None): """ Creates a new repository, and returns the created repository. @@ -123,6 +124,7 @@ def create_repo(self, auth, name, description=None, private=False, auto_init=Fal :param list[str] gitignore_templates: collection of ``.gitignore`` templates to apply :param str license_template: license template to apply :param str readme_template: README template to apply + :param str organization: organization under which repository is created :return: a representation of the created repository :rtype: GogsRepo :raises NetworkFailure: if there is an error communicating with the server @@ -140,7 +142,8 @@ def create_repo(self, auth, name, description=None, private=False, auto_init=Fal "readme": readme_template } data = {k: v for (k, v) in data.items() if v is not None} - response = self._post("/user/repos", auth=auth, data=data) + url = "/org/{0}/repos".format(organization) if organization else "/user/repos" + response = self._post(url, auth=auth, data=data) return GogsRepo.from_json(self._check_ok(response).json()) def repo_exists(self, auth, username, repo_name): From 5787c9a4728476ae049b76715e9244adfe7ea12d Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Thu, 30 Mar 2017 23:42:59 +0300 Subject: [PATCH 02/33] New line at end of file --- gogs_client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gogs_client/entities.py b/gogs_client/entities.py index 956a799..a117a0f 100644 --- a/gogs_client/entities.py +++ b/gogs_client/entities.py @@ -239,4 +239,4 @@ def pull(self): :rtype: bool """ - return self._pull \ No newline at end of file + return self._pull From bd7ac3a634d48606347a35f984cf21c503c0231b Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Fri, 31 Mar 2017 02:23:03 +0300 Subject: [PATCH 03/33] Webhook entity representation. --- docs/entities.rst | 3 ++ docs/updates.rst | 6 +++ gogs_client/entities.py | 67 ++++++++++++++++++++++++++++++++++ gogs_client/interface.py | 63 ++++++++++++++++++++++++++++++++ gogs_client/updates.py | 79 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+) diff --git a/docs/entities.rst b/docs/entities.rst index 8e37c59..63fb131 100644 --- a/docs/entities.rst +++ b/docs/entities.rst @@ -17,3 +17,6 @@ This pages documents classes provided by ``gogs_client`` module that represent e .. autoclass:: gogs_client.entities::GogsRepo.Permissions() :members: + +.. autoclass:: gogs_client.entities::GogsRepo.Hook() + :members: diff --git a/docs/updates.rst b/docs/updates.rst index b1b1d13..e196ade 100644 --- a/docs/updates.rst +++ b/docs/updates.rst @@ -8,3 +8,9 @@ Updates .. autoclass:: gogs_client.updates::GogsUserUpdate.Builder() :members: + +.. autoclass:: GogsHookUpdate() + :members: + +.. autoclass:: gogs_client.updates::GogsHookUpdate.Builder() + :members: \ No newline at end of file diff --git a/gogs_client/entities.py b/gogs_client/entities.py index a117a0f..4c32718 100644 --- a/gogs_client/entities.py +++ b/gogs_client/entities.py @@ -240,3 +240,70 @@ def pull(self): :rtype: bool """ return self._pull + + class Hook(object): + def __init__(self, hook_id, hook_type, events, active, config): + self._id = hook_id + self._type = hook_type + self._events = events + self._active = active + self._config = config + + @staticmethod + def from_json(parsed_json): + hook_id = json_get(parsed_json, "id") + hook_type = json_get(parsed_json, "type") + events = json_get(parsed_json, "events") + active = json_get(parsed_json, "active") + config = json_get(parsed_json, "config") + + return GogsRepo.Hook(hook_id=hook_id, hook_type=hook_type, events=events, active=active, + config=config) + + @property # named hook_id to avoid conflict with built-in id + def hook_id(self): + """ + The hook's id number + + :rtype: int + """ + return self._id + + @property # named hook_type to avoid conflict with built-in type + def hook_type(self): + """ + The hook's type (gogs, slack, etc.) + + :rtype: str + """ + return self._type + + @property + def events(self): + """ + The events that fires the hook + + :rtype: list of strs + """ + return self._events + + @property + def active(self): + """ + State of the hook + + :rtype: bool + """ + return self._active + + @property + def config(self): + """ + Config of the hook. Contains max. 3 keys: + - content_type + - url + - secret + + :rtype: dict + """ + return self._config diff --git a/gogs_client/interface.py b/gogs_client/interface.py index 17892d5..6590e90 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -290,6 +290,69 @@ def delete_user(self, auth, username): path = "/admin/users/{}".format(username) self._check_ok(self._delete(path, auth=auth)) + def get_repo_hooks(self, auth, username, repo_name): + """ + Returns all hooks of repository with name ``repo_name`` owned by + the user with username ``username``. + + :param auth.Authentication auth: authentication object + :param str username: username of owner of repository + :param str repo_name: name of repository + :return: a list of hooks for the specified repository + :rtype: List[GogsRepo.Hooks] + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + path = "/repos/{u}/{r}/hooks".format(u=username, r=repo_name) + response = self._check_ok(self._get(path, auth=auth)) + hooks = [GogsRepo.Hook.from_json(hook) for hook in response.json()] + return hooks + + def create_hook(self, auth, repo_name, hook_type, config, events=["push"], organization=None, active=False): + """ + Creates a new hook, and returns the created hook. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str repo_name: the name of the repo for which we create the hook + :param str hook_type: The type of webhook, either "gogs" or "slack" + :param dict config: Key/value pairs to provide settings for this hook ("url", "content_type", "secret") + :param list events: Determines what events the hook is triggered for. Default: ["push"] + :param str organization: (Optional) Organization of the repo + :param bool active: Determines whether the hook is actually triggered on pushes. Default is false + :return: a representation of the created hook + :rtype: GogsRepo.Hook + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + + data = { + "type": hook_type, + "config": config, + "events": events, + "active": active + } + + url = "/repos/{o}/{r}/hooks".format(o=organization, r=repo_name) if organization else "/repos/{r}/hooks".format(r=repo_name) + response = self._post(url, auth=auth, data=data) + print response.text + self._check_ok(response) + return GogsRepo.Hook.from_json(response.json()) + + def delete_hook(self, auth, username, repo_name, hook_id): + """ + Deletes the hook with id ``hook_id`` for repo with name ``repo_name`` + owned by the user with username ``username``. + + :param auth.Authentication auth: authentication object + :param str username: username of owner of repository + :param str repo_name: name of repository of hook to delete + :param int hook_id: id of hook to delete + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + path = "/repos/{u}/{r}/hooks/{i}".format(u=username, r=repo_name, i=hook_id) + self._check_ok(self._delete(path, auth=auth)) + # Helper methods def _delete(self, path, auth=None, **kwargs): diff --git a/gogs_client/updates.py b/gogs_client/updates.py index f31d7e2..8a66ab1 100644 --- a/gogs_client/updates.py +++ b/gogs_client/updates.py @@ -182,3 +182,82 @@ def build(self): admin=self._admin, allow_git_hook=self._allow_git_hook, allow_import_local=self._allow_import_local) + + +class GogsHookUpdate(object): + """ + An immutable represention of a collection of Gogs hook attributes to update. + + Instances should be created using the :class:`~GogsHookUpdate.Builder` class. + """ + def __init__(self, hook_type, events, config, active): + """ + :param hook_type: + :param events: + :param config: + :param active: + """ + + self._type = hook_type + self._events = events + self._config = config + self._active = active + + def as_dict(self): + fields = { + "type": self._type, + "events": self._events, + "config": self._config, + "active": self._active, + } + return {k: v for (k, v) in fields.items() if v is not None} + + class Builder(object): + def __init__(self): + """ + :param str login_name: login name for authentication source + :param str email: email address of user to update + """ + self._type = None + self._events = None + self._config = None + self._active = None + + + def set_events(self, events): + """ + :param list events: + :return: the updated builder + :rtype: GogsHookUpdate.Builder + """ + self._events = events + return self + + def set_config(self, config): + """ + :param dict config: + :return: the updated builder + :rtype: GogsHookUpdate.Builder + """ + self._config = config + return self + + def set_active(self, active): + """ + :param bool active: + :return: the updated builder + :rtype: GogsHookUpdate.Builder + """ + self._active = active + return self + + def build(self): + """ + :return: A :class:`~GogsHookUpdate` instance reflecting the changes added to the builder. + :rtype: GogsHookUpdate + """ + return GogsHookUpdate( + hook_type=self._type, + events=self._events, + config=self._config, + active=self._active) From f1bdd3a23e347ca8934dcdb5db9b40ccbd4cb378 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Fri, 31 Mar 2017 16:00:39 +0300 Subject: [PATCH 04/33] Create, update and delete hooks. --- gogs_client/_implementation/http_utils.py | 6 +++--- gogs_client/entities.py | 10 ++++++++++ gogs_client/interface.py | 17 ++++++++++++++++- gogs_client/updates.py | 8 +------- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/gogs_client/_implementation/http_utils.py b/gogs_client/_implementation/http_utils.py index 4114e63..6dc29e8 100644 --- a/gogs_client/_implementation/http_utils.py +++ b/gogs_client/_implementation/http_utils.py @@ -36,13 +36,13 @@ def options(self, relative_path, params=None, **kwargs): return self.session.options(self.absolute_url(relative_path), params=params, **kwargs) def patch(self, relative_path, data=None, **kwargs): - return self.session.patch(self.absolute_url(relative_path), data=data, **kwargs) + return self.session.patch(self.absolute_url(relative_path), json=data, **kwargs) def post(self, relative_path, data=None, **kwargs): - return self.session.post(self.absolute_url(relative_path), data=data, **kwargs) + return self.session.post(self.absolute_url(relative_path), json=data, **kwargs) def put(self, relative_path, params=None, data=None, **kwargs): - return self.session.put(self.absolute_url(relative_path), params=params, data=data, **kwargs) + return self.session.put(self.absolute_url(relative_path), params=params, json=data, **kwargs) def append_url(base_url, path): diff --git a/gogs_client/entities.py b/gogs_client/entities.py index 4c32718..63f14a1 100644 --- a/gogs_client/entities.py +++ b/gogs_client/entities.py @@ -260,6 +260,16 @@ def from_json(parsed_json): return GogsRepo.Hook(hook_id=hook_id, hook_type=hook_type, events=events, active=active, config=config) + def as_dict(self): + fields = { + "id": self._id, + "type": self._type, + "events": self._events, + "config": self._config, + "active": self._active, + } + return {k: v for (k, v) in fields.items() if v is not None} + @property # named hook_id to avoid conflict with built-in id def hook_id(self): """ diff --git a/gogs_client/interface.py b/gogs_client/interface.py index 6590e90..7e7716e 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -334,10 +334,25 @@ def create_hook(self, auth, repo_name, hook_type, config, events=["push"], organ url = "/repos/{o}/{r}/hooks".format(o=organization, r=repo_name) if organization else "/repos/{r}/hooks".format(r=repo_name) response = self._post(url, auth=auth, data=data) - print response.text self._check_ok(response) return GogsRepo.Hook.from_json(response.json()) + def update_hook(self, auth, repo_name, hook_id, update, organization=None): + """ + Updates hook with id ``hook_id`` according to ``update``. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str repo_name: repo of the hook to update + :param GogsHookUpdate update: a ``GogsHookUpdate`` object describing the requested update + :return: the updated hook + :rtype: GogsRepo.Hook + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + path = "/repos/{o}/{r}/hooks/{i}".format(o=organization, r=repo_name, i=hook_id) if organization else "/repos/{r}/hooks/{i}".format(r=repo_name, i=hook_id) + response = self._check_ok(self._patch(path, auth=auth, data=update.as_dict())) + return GogsRepo.Hook.from_json(response.json()) + def delete_hook(self, auth, username, repo_name, hook_id): """ Deletes the hook with id ``hook_id`` for repo with name ``repo_name`` diff --git a/gogs_client/updates.py b/gogs_client/updates.py index 8a66ab1..e45754a 100644 --- a/gogs_client/updates.py +++ b/gogs_client/updates.py @@ -190,22 +190,19 @@ class GogsHookUpdate(object): Instances should be created using the :class:`~GogsHookUpdate.Builder` class. """ - def __init__(self, hook_type, events, config, active): + def __init__(self, events, config, active): """ - :param hook_type: :param events: :param config: :param active: """ - self._type = hook_type self._events = events self._config = config self._active = active def as_dict(self): fields = { - "type": self._type, "events": self._events, "config": self._config, "active": self._active, @@ -218,12 +215,10 @@ def __init__(self): :param str login_name: login name for authentication source :param str email: email address of user to update """ - self._type = None self._events = None self._config = None self._active = None - def set_events(self, events): """ :param list events: @@ -257,7 +252,6 @@ def build(self): :rtype: GogsHookUpdate """ return GogsHookUpdate( - hook_type=self._type, events=self._events, config=self._config, active=self._active) From 4c2c4773b2e8e613e3453aad1e0d4e08dacd3060 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 3 Apr 2017 15:19:59 +0300 Subject: [PATCH 05/33] Added GogsHookUpdate in import --- gogs_client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gogs_client/__init__.py b/gogs_client/__init__.py index 1112302..ffdbc1a 100644 --- a/gogs_client/__init__.py +++ b/gogs_client/__init__.py @@ -1,4 +1,4 @@ from gogs_client.auth import Authentication, Token, UsernamePassword from gogs_client.entities import GogsUser, GogsRepo from gogs_client.interface import GogsApi, ApiFailure, NetworkFailure -from gogs_client.updates import GogsUserUpdate +from gogs_client.updates import GogsUserUpdate, GogsHookUpdate From abd3c01de7771b0df69c6f1b6a80182c4694c9bf Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 3 Apr 2017 15:20:28 +0300 Subject: [PATCH 06/33] Created tests for hook creation and update --- tests/interface_test.py | 65 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/interface_test.py b/tests/interface_test.py index a2d0be8..63b43ce 100644 --- a/tests/interface_test.py +++ b/tests/interface_test.py @@ -49,8 +49,26 @@ def setUp(self): }""" self.username_password = gogs_client.UsernamePassword( "auth_username", "password") + self.hook_json_str = """{ + "id": 4, + "type": "gogs", + "config": { + "content_type": "json", + "url": "http://test.io/hook" + }, + "events": [ + "create", + "push", + "issues" + ], + "active": false, + "updated_at": "2017-03-31T12:42:58Z", + "created_at": "2017-03-31T12:42:58Z" + }""" + self.expected_repo = gogs_client.GogsRepo.from_json(json.loads(self.repo_json_str)) self.expected_user = gogs_client.GogsUser.from_json(json.loads(self.user_json_str)) + self.expected_hook = gogs_client.GogsRepo.Hook.from_json(json.loads(self.hook_json_str)) self.token = gogs_client.Token.from_json(json.loads(self.token_json_str)) @responses.activate @@ -184,7 +202,7 @@ def test_update_user1(self): .build() def callback(request): - data = self.data_of_query(request.body) + data = json.loads(request.body) self.assertEqual(data["login_name"], "loginname") self.assertEqual(data["full_name"], "Example User") self.assertEqual(data["email"], "user@example.com") @@ -267,6 +285,45 @@ def test_ensure_auth_token(self): token = self.client.ensure_token(self.username_password, self.token.name) self.assert_tokens_equals(token, self.token) + @responses.activate + def test_create_hook1(self): + uri = self.path("/repos/username/repo1/hooks") + responses.add(responses.POST, uri, body=self.hook_json_str) + hook = self.client.create_hook(self.token, + repo_name="repo1", + hook_type="gogs", + config={ + "content_type": "json2", + "url": "http://test.io/hook" + }, + events=["create", "push", "issues"], + active=False, + organization="username") + self.assert_hooks_equals(hook, self.expected_hook) + self.assertEqual(len(responses.calls), 1) + call = responses.calls[0] + self.assertEqual(call.request.url, self.path_with_token(uri)) + + @responses.activate + def test_update_hook1(self): + update = gogs_client.GogsHookUpdate.Builder()\ + .set_events(["issues_comments"])\ + .set_config({"url": "http://newurl.com/hook"})\ + .set_active(True)\ + .build() + + def callback(request): + data = json.loads(request.body) + self.assertEqual(data["config"]["url"], "http://newurl.com/hook") + self.assertEqual(data["events"], ['issues_comments']) + self.assertEqual(data["active"], True) + return 200, {}, self.hook_json_str + uri = self.path("/repos/username/repo1/hooks/4") + responses.add_callback(responses.PATCH, uri, callback=callback) + hook = self.client.update_hook(self.token, "repo1", 4, update, organization="username") + self.assert_hooks_equals(hook, self.expected_hook) + + # helper methods @staticmethod @@ -310,6 +367,12 @@ def assert_tokens_equals(self, token, expected): self.assertEqual(token.name, expected.name) self.assertEqual(token.token, expected.token) + def assert_hooks_equals(self, hook, expected): + self.assertEqual(hook.hook_id, expected.hook_id) + self.assertEqual(hook.hook_type, expected.hook_type) + self.assertEqual(hook.events, expected.events) + self.assertEqual(hook.config, expected.config) + self.assertEqual(hook.active, expected.active) if __name__ == "__main__": unittest.main() From 95e368de8ed7d84ddc77666618c4cea1ed640120 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 3 Apr 2017 15:47:09 +0300 Subject: [PATCH 07/33] Fix for python3 in test_update_user1 and test_update_hook1 --- tests/interface_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/interface_test.py b/tests/interface_test.py index 63b43ce..e8de12d 100644 --- a/tests/interface_test.py +++ b/tests/interface_test.py @@ -202,7 +202,7 @@ def test_update_user1(self): .build() def callback(request): - data = json.loads(request.body) + data = json.loads(request.body.decode('utf8')) self.assertEqual(data["login_name"], "loginname") self.assertEqual(data["full_name"], "Example User") self.assertEqual(data["email"], "user@example.com") @@ -313,7 +313,7 @@ def test_update_hook1(self): .build() def callback(request): - data = json.loads(request.body) + data = json.loads(request.body.decode('utf8')) self.assertEqual(data["config"]["url"], "http://newurl.com/hook") self.assertEqual(data["events"], ['issues_comments']) self.assertEqual(data["active"], True) From 403d252b518aad4b6b0baba606235022da879087 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 3 Apr 2017 18:00:43 +0300 Subject: [PATCH 08/33] Test hooks list and delete. --- gogs_client/interface.py | 3 ++- tests/interface_test.py | 47 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/gogs_client/interface.py b/gogs_client/interface.py index 7e7716e..89cd814 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -366,7 +366,8 @@ def delete_hook(self, auth, username, repo_name, hook_id): :raises ApiFailure: if the request cannot be serviced """ path = "/repos/{u}/{r}/hooks/{i}".format(u=username, r=repo_name, i=hook_id) - self._check_ok(self._delete(path, auth=auth)) + response = self._check_ok(self._delete(path, auth=auth)) + return response # Helper methods diff --git a/tests/interface_test.py b/tests/interface_test.py index e8de12d..be93a39 100644 --- a/tests/interface_test.py +++ b/tests/interface_test.py @@ -65,7 +65,38 @@ def setUp(self): "updated_at": "2017-03-31T12:42:58Z", "created_at": "2017-03-31T12:42:58Z" }""" - + self.hooks_list_json_str = """[ + { + "id": 4, + "type": "gogs", + "config": { + "content_type": "json", + "url": "http://test.io/hook" + }, + "events": [ + "create", + "push", + "issues" + ], + "active": false, + "updated_at": "2017-03-31T12:42:58Z", + "created_at": "2017-03-31T12:42:58Z" + }, + { + "id": 3, + "type": "gogs", + "config": { + "content_type": "json", + "url": "http://192.168.201.1:8080/hook22/" + }, + "events": [ + "issue_comment" + ], + "active": true, + "updated_at": "2017-03-31T12:47:56Z", + "created_at": "2017-03-31T12:42:54Z" + } + ]""" self.expected_repo = gogs_client.GogsRepo.from_json(json.loads(self.repo_json_str)) self.expected_user = gogs_client.GogsUser.from_json(json.loads(self.user_json_str)) self.expected_hook = gogs_client.GogsRepo.Hook.from_json(json.loads(self.hook_json_str)) @@ -323,6 +354,20 @@ def callback(request): hook = self.client.update_hook(self.token, "repo1", 4, update, organization="username") self.assert_hooks_equals(hook, self.expected_hook) + @responses.activate + def test_list_hooks(self): + uri = self.path("/repos/username/repo1/hooks") + responses.add(responses.GET, uri, body=self.hooks_list_json_str, status=20) + hooks = self.client.get_repo_hooks(self.token, "username", "repo1") + self.assertEqual(len(hooks), 2) + self.assert_hooks_equals(hooks[0], self.expected_hook) + + @responses.activate + def test_delete_hook(self): + uri = self.path("/repos/username/repo1/hooks/4") + responses.add(responses.DELETE, uri, status=204) + hook = self.client.delete_hook(self.token, "username", "repo1", 4) + self.assertEqual(hook.status_code, 204) # helper methods From 428cd5621d8ca3b2b9534853990d055e7084a76a Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 3 Apr 2017 20:24:03 +0300 Subject: [PATCH 09/33] Created Organization and Team entity --- gogs_client/entities.py | 166 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/gogs_client/entities.py b/gogs_client/entities.py index 63f14a1..73b9509 100644 --- a/gogs_client/entities.py +++ b/gogs_client/entities.py @@ -317,3 +317,169 @@ def config(self): :rtype: dict """ return self._config + + +class GogsOrg(object): + """ + An immutable representation of a Gogs Organization. + """ + def __init__(self, org_id, username, full_name, avatar_url, description, website, location): + self._id = org_id + self._username = username + self._full_name = full_name + self._avatar_url = avatar_url + self._description = description + self._website = website + self._location = location + + @staticmethod + def from_json(parsed_json): + org_id = json_get(parsed_json, "id") + username = json_get(parsed_json, "username") + full_name = json_get(parsed_json, "full_name") + avatar_url = json_get(parsed_json, "avatar_url") + description = json_get(parsed_json, "description") + website = json_get(parsed_json, "website") + location = json_get(parsed_json, "location") + return GogsOrg(org_id=org_id, username=username, full_name=full_name, + avatar_url=avatar_url, description=description, + website=website, location=location) + + def as_dict(self): + fields = { + "id": self._id, + "username": self._username, + "full_name": self._full_name, + "avatar_url": self._avatar_url, + "description": self._description, + "website": self._website, + "location": self._location + } + return {k: v for (k, v) in fields.items() if v is not None} + + @property # named org_id to avoid conflict with built-in id + def org_id(self): + """ + The organization's id + + :rtype: int + """ + return self._id + + @property + def username(self): + """ + Organization's username + + :rtype: int + """ + return self._username + + @property + def full_name(self): + """ + Organization's full name + + :rtype: int + """ + return self._full_name + + @property + def avatar_url(self): + """ + Organization's avatar url + + :rtype: str + """ + return self._avatar_url + + @property + def description(self): + """ + Organization's description + + :rtype: str + """ + return self._description + + @property + def website(self): + """ + Organization's website address + + :rtype: str + """ + return self._website + + @property + def location(self): + """ + Organization's location + + :rtype: str + """ + return self._location + + class Team(object): + """ + Team of an organization + """ + def __init__(self, team_id, name, description, permission): + self._id = team_id + self._name = name + self._description = description + self._permission = permission + + @staticmethod + def from_json(parsed_json): + team_id = json_get(parsed_json, "id") + name = json_get(parsed_json, "name") + description = json_get(parsed_json, "description") + permission = json_get(parsed_json, "permission") + return GogsOrg.Team(team_id=team_id, name=name, description=description, permission=permission) + + def as_dict(self): + fields = { + "team_id": self._id, + "name": self._name, + "description": self._description, + "permission": self._permission, + } + return {k: v for (k, v) in fields.items() if v is not None} + + @property # named team_id to avoid conflict with built-in id + def team_id(self): + """ + Team's id + + :rtype: int + """ + return self._id + + @property + def name(self): + """ + Team name + + :rtype: int + """ + return self._name + + @property + def description(self): + """ + Description to the team + + :rtype: int + """ + return self._description + + @property + def permission(self): + """ + Team permission, can be read, write or admin, default is read + + :rtype: int + """ + return self._permission + From 89c154c6d5cd86382365017dfbfe3aa683fbf686 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 3 Apr 2017 20:24:24 +0300 Subject: [PATCH 10/33] Created all administration for organization --- gogs_client/interface.py | 132 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/gogs_client/interface.py b/gogs_client/interface.py index 89cd814..b1bc502 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -1,7 +1,7 @@ import requests from gogs_client._implementation.http_utils import RelativeHttpRequestor, append_url -from gogs_client.entities import GogsUser, GogsRepo +from gogs_client.entities import GogsUser, GogsRepo, GogsOrg from gogs_client.auth import Token @@ -369,6 +369,126 @@ def delete_hook(self, auth, username, repo_name, hook_id): response = self._check_ok(self._delete(path, auth=auth)) return response + def create_organization(self, auth, username, org_name, full_name=None, avatar_url=None, description=None, website=None, location=None): + """ + Creates a new organization, and returns the created one. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str username: [Required] Organization user name + :param str full_name: Full name of organization + :param str description: Description to the organization + :param str website: Official website + :param str location: Organization location + :return: a representation of the created organization + :rtype: GogsOrg + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + + data = { + "username": org_name, + "full_name": full_name, + "description": description, + "website": website, + "location": location + } + + url = "/admin/users/{u}/orgs".format(u=username) + response = self._post(url, auth=auth, data=data) + self._check_ok(response) + return GogsOrg.from_json(response.json()) + + def create_organization_team(self, auth, org_name, name, description=None, permission="read"): + """ + Creates a new team of the organization. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str org_name: [Required] Organization user name + :param str team_name: Full name of the team + :param str description: Description to the team + :param str permission: Team permission, can be read, write or admin, default is read + :return: a representation of the created team + :rtype: GogsOrg.Team + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + + data = { + "name": name, + "description": description, + "permission": permission + } + + url = "/admin/orgs/{o}/teams".format(o=org_name) + response = self._post(url, auth=auth, data=data) + self._check_ok(response) + return GogsOrg.Team.from_json(response.json()) + + def add_team_membership(self, auth, team_id, username): + """ + Add user to team. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str team_id: [Required] Id of the team + :param str username: Username of the user to be added to team + :return: status code of the request + :rtype: str + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + url = "/admin/teams/{t}/members/{u}".format(t=team_id, u=username) + response = self._put(url, auth=auth) + return self._check_ok(response) + + def remove_team_membership(self, auth, team_id, username): + """ + Remove user from team. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str team_id: [Required] Id of the team + :param str username: Username of the user to be removed from the team + :return: status code of the request + :rtype: str + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + url = "/admin/teams/{t}/members/{u}".format(t=team_id, u=username) + response = self._delete(url, auth=auth) + return self._check_ok(response) + + def add_repo_to_team(self, auth, team_id, repo_name): + """ + Add or update repo from team. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str team_id: [Required] Id of the team + :param str repo_name: Name of the repo to be added to the team + :return: status code of the request + :rtype: str + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + url = "/admin/teams/{t}/repos/{r}".format(t=team_id, r=repo_name) + response = self._put(url, auth=auth) + return self._check_ok(response) + + + def remove_repo_from_team(self, auth, team_id, repo_name): + """ + Remove repo from team. + + :param auth.Authentication auth: authentication object, must be admin-level + :param str team_id: [Required] Id of the team + :param str repo_name: Name of the repo to be removed from the team + :return: status code of the request + :rtype: str + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + url = "/admin/teams/{t}/repos/{r}".format(t=team_id, r=repo_name) + response = self._delete(url, auth=auth) + return self._check_ok(response) + # Helper methods def _delete(self, path, auth=None, **kwargs): @@ -403,6 +523,14 @@ def _post(self, path, auth=None, **kwargs): except requests.RequestException as exc: raise NetworkFailure(exc) + def _put(self, path, auth=None, **kwargs): + if auth is not None: + auth.update_kwargs(kwargs) + try: + return self._requestor.put(path, **kwargs) + except requests.RequestException as exc: + raise NetworkFailure(exc) + @staticmethod def _check_ok(response): """ @@ -419,7 +547,7 @@ def _fail(response): """ message = "Status code: {}-{}, url: {}".format(response.status_code, response.reason, response.url) try: - message += ", message:{}".format(response.json()[0]["message"]) + message += ", message:{}".format(response.json()["message"]) except Exception: pass raise ApiFailure(message, response.status_code) From 6a9e52725411c2b0876c5b701b016b31db7e7be7 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 4 Apr 2017 12:56:40 +0300 Subject: [PATCH 11/33] Added GogsOrg --- docs/entities.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/entities.rst b/docs/entities.rst index 63fb131..8820384 100644 --- a/docs/entities.rst +++ b/docs/entities.rst @@ -20,3 +20,9 @@ This pages documents classes provided by ``gogs_client`` module that represent e .. autoclass:: gogs_client.entities::GogsRepo.Hook() :members: + +.. autoclass:: gogs_client.entities::GogsOrg() + :members: + +.. autoclass:: gogs_client.entities::GogsOrg.Team() + :members: \ No newline at end of file From b66c5b80d38abae88ba9bffe73f43549cdbf12cd Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 4 Apr 2017 12:57:17 +0300 Subject: [PATCH 12/33] Import GogsOrg --- gogs_client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gogs_client/__init__.py b/gogs_client/__init__.py index ffdbc1a..9f60d7e 100644 --- a/gogs_client/__init__.py +++ b/gogs_client/__init__.py @@ -1,4 +1,4 @@ from gogs_client.auth import Authentication, Token, UsernamePassword -from gogs_client.entities import GogsUser, GogsRepo +from gogs_client.entities import GogsUser, GogsRepo, GogsOrg from gogs_client.interface import GogsApi, ApiFailure, NetworkFailure from gogs_client.updates import GogsUserUpdate, GogsHookUpdate From aa93e863289afebd6636770a92a25533a6b38cc1 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 4 Apr 2017 12:57:45 +0300 Subject: [PATCH 13/33] Added tests for Administration Organization --- tests/interface_test.py | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/interface_test.py b/tests/interface_test.py index be93a39..eda26f1 100644 --- a/tests/interface_test.py +++ b/tests/interface_test.py @@ -97,9 +97,26 @@ def setUp(self): "created_at": "2017-03-31T12:42:54Z" } ]""" + self.org_json_str = """{ + "id": 7, + "username": "gogs2", + "full_name": "Gogs2", + "avatar_url": "/avatars/7", + "description": "Gogs is a painless self-hosted Git Service.", + "website": "https://gogs.io", + "location": "USA" + }""" + self.team_json_str = """{ + "id": 12, + "name": "new-team", + "description": "A new team created by API", + "permission": "write" + }""" self.expected_repo = gogs_client.GogsRepo.from_json(json.loads(self.repo_json_str)) self.expected_user = gogs_client.GogsUser.from_json(json.loads(self.user_json_str)) self.expected_hook = gogs_client.GogsRepo.Hook.from_json(json.loads(self.hook_json_str)) + self.expected_org = gogs_client.GogsOrg.from_json(json.loads(self.org_json_str)) + self.expected_team = gogs_client.GogsOrg.Team.from_json(json.loads(self.team_json_str)) self.token = gogs_client.Token.from_json(json.loads(self.token_json_str)) @responses.activate @@ -369,6 +386,64 @@ def test_delete_hook(self): hook = self.client.delete_hook(self.token, "username", "repo1", 4) self.assertEqual(hook.status_code, 204) + @responses.activate + def test_create_organization(self): + uri = self.path("/admin/users/username/orgs") + responses.add(responses.POST, uri, body=self.org_json_str) + org = self.client.create_organization(self.token, + username="username", + org_name="gogs2", + full_name="Gogs2", + description="Gogs is a painless self-hosted Git Service.", + website="https://gogs.io", + location="USA") + self.assert_org_equals(org, self.expected_org) + self.assertEqual(len(responses.calls), 1) + call = responses.calls[0] + self.assertEqual(call.request.url, self.path_with_token(uri)) + + @responses.activate + def test_create_organization_team(self): + uri = self.path("/admin/orgs/username/teams") + responses.add(responses.POST, uri, body=self.team_json_str) + team = self.client.create_organization_team(self.token, + org_name="username", + name="new-team", + description="A new team created by API", + permission="write") + self.assert_team_equals(team, self.expected_team) + self.assertEqual(len(responses.calls), 1) + call = responses.calls[0] + self.assertEqual(call.request.url, self.path_with_token(uri)) + + @responses.activate + def test_add_team_membership(self): + uri = self.path("/admin/teams/team/members/username") + responses.add(responses.PUT, uri, status=204) + resp = self.client.add_team_membership(self.token, "team", "username") + self.assertEqual(resp.status_code, 204) + + @responses.activate + def test_remove_team_membership(self): + uri = self.path("/admin/teams/team/members/username") + responses.add(responses.DELETE, uri, status=204) + resp = self.client.remove_team_membership(self.token, "team", "username") + self.assertEqual(resp.status_code, 204) + + @responses.activate + def test_add_repo_to_team(self): + uri = self.path("/admin/teams/test_team/repos/repo_name") + responses.add(responses.PUT, uri, status=204) + resp = self.client.add_repo_to_team(self.token, "test_team", "repo_name") + self.assertEqual(resp.status_code, 204) + + @responses.activate + def test_remove_repo_from_team(self): + uri = self.path("/admin/teams/test_team/repos/repo_name") + responses.add(responses.DELETE, uri, status=204) + resp = self.client.remove_repo_from_team(self.token, "test_team", "repo_name") + self.assertEqual(resp.status_code, 204) + # helper methods @staticmethod @@ -419,5 +494,20 @@ def assert_hooks_equals(self, hook, expected): self.assertEqual(hook.config, expected.config) self.assertEqual(hook.active, expected.active) + def assert_org_equals(self, org, expected): + self.assertEqual(org.org_id, expected.org_id) + self.assertEqual(org.username, expected.username) + self.assertEqual(org.full_name, expected.full_name) + self.assertEqual(org.avatar_url, expected.avatar_url) + self.assertEqual(org.description, expected.description) + self.assertEqual(org.website, expected.website) + self.assertEqual(org.location, expected.location) + + def assert_team_equals(self, team, expected): + self.assertEqual(team.team_id, expected.team_id) + self.assertEqual(team.name, expected.name) + self.assertEqual(team.description, expected.description) + self.assertEqual(team.permission, expected.permission) + if __name__ == "__main__": unittest.main() From 557b267f3afbfc8988c079fddfb8913b35d96afd Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Wed, 5 Apr 2017 00:13:37 +0300 Subject: [PATCH 14/33] Added model for deployment key --- gogs_client/entities.py | 83 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/gogs_client/entities.py b/gogs_client/entities.py index 73b9509..6c16ddd 100644 --- a/gogs_client/entities.py +++ b/gogs_client/entities.py @@ -318,6 +318,89 @@ def config(self): """ return self._config + class DeployKey(object): + def __init__(self, key_id, key, url, title, created_at, read_only): + self._id = key_id + self._key = key + self._url = url + self._title = title + self._created_at = created_at + self._read_only = read_only + + @staticmethod + def from_json(parsed_json): + key_id = json_get(parsed_json, "id") + key = json_get(parsed_json, "key") + url = json_get(parsed_json, "url") + title = json_get(parsed_json, "title") + created_at = json_get(parsed_json, "created_at") + read_only = json_get(parsed_json, "read_only") + + return GogsRepo.DeployKey(key_id=key_id, key=key, url=url, + title=title, created_at=created_at, read_only=read_only) + + def as_dict(self): + fields = { + "id": self._id, + "key": self._key, + "url": self._url, + "title": self._title, + "created_at": self._created_at, + "read_only": self._read_only, + } + return {k: v for (k, v) in fields.items() if v is not None} + + @property # named key_id to avoid conflict with built-in id + def key_id(self): + """ + The key's id number + + :rtype: int + """ + return self._id + + @property + def key(self): + """ + The content of the key + + :rtype: str + """ + return self._key + + @property + def url(self): + """ + Url where the key can be found + + :rtype: str + """ + return self._url + + @property + def title(self): + """ + The name of the key + + :rtype: str + """ + return self._title + + @property + def created_at(self): + """ + Creation date of the key. + :rtype: str + """ + return self._created_at + + @property + def read_only(self): + """ + Is the key read only? + :rtype: bool + """ + return self._read_only class GogsOrg(object): """ From 42881da37c17ddd852f5465ecb710563e576aee6 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Wed, 5 Apr 2017 00:14:16 +0300 Subject: [PATCH 15/33] Added methods for deploy keys --- gogs_client/interface.py | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/gogs_client/interface.py b/gogs_client/interface.py index b1bc502..61c8c8b 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -489,6 +489,72 @@ def remove_repo_from_team(self, auth, team_id, repo_name): response = self._delete(url, auth=auth) return self._check_ok(response) + def list_deploy_keys(self, auth, username, repo_name): + """ + List deploy keys. + + :param str username: username or organization + :param str repo_name: the name of the repo + :return: a list of deploy keys for the repo + :rtype: List[GogsRepo.DeployKey] + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + response = self._check_ok(self._get("/repos/{u}/{r}/keys".format(u=username, r=repo_name),auth=auth)) + return [GogsRepo.DeployKey.from_json(key_json) for key_json in response.json()] + + def get_deploy_key(self, auth, username, repo_name, key_id): + """ + Get deploy key for specific repo. + + :param str username: username or organization + :param str repo_name: the name of the repo + :param int key_id: the id of the key + :return: the deploy key + :rtype: GogsRepo.DeployKey + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + + response = self._check_ok(self._get("/repos/{u}/{r}/keys/{k}".format(u=username, r=repo_name, k=key_id), auth=auth)) + return GogsRepo.DeployKey.from_json(response.json()) + + def add_deploy_key(self, auth, username, repo_name, title, key): + """ + Get deploy key for specific repo. + + :param str username: username or organization + :param str repo_name: the name of the repo + :param int key_id: the id of the key + :return: the deploy key + :rtype: GogsRepo.DeployKey + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + data = { + "title": title, + "key": key + } + response = self._check_ok(self._post("/repos/{u}/{r}/keys".format(u=username, r=repo_name), auth=auth, data=data)) + return GogsRepo.DeployKey.from_json(response.json()) + + def delete_deploy_key(self, auth, username, repo_name, key_id): + """ + Remove deploy key for specific repo. + + :param str username: username or organization + :param str repo_name: the name of the repo + :param int key_id: the id of the key + :return: the deploy key + :rtype: GogsRepo.DeployKey + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + + response = self._check_ok(self._delete("/repos/{u}/{r}/keys/{k}".format(u=username, r=repo_name, k=key_id), auth=auth)) + return self._check_ok(response) + + # Helper methods def _delete(self, path, auth=None, **kwargs): From 2dd75ee4a7f15df537ba58da439041cfd5a557fd Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Wed, 5 Apr 2017 00:14:34 +0300 Subject: [PATCH 16/33] Added tests for deploy keys --- tests/interface_test.py | 66 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/tests/interface_test.py b/tests/interface_test.py index eda26f1..e8b6522 100644 --- a/tests/interface_test.py +++ b/tests/interface_test.py @@ -112,11 +112,35 @@ def setUp(self): "description": "A new team created by API", "permission": "write" }""" + self.deploy_key_json_str = """{ + "id": 1, + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUbmwBOG5vI8qNCztby5LDc9ozwTuwsqf+1fpuHjT9iQ2Lu9nlKHQJcPSgdrYAcc+88K6o74ayhTAjfajKxkIHnbzZFjidoVZSQDhX5qvl93jvY/Uz390qky0sweW+fspm8pRJL+ofE3QEN5AXAuycq1tgsRT32XC+Ta82Xyv8b3xW+pWbsZzYCzUsZXDe/xWxg1rndXh2BIrmcYf9BMiv9ZJIojJXfuLCeRXl550tDzaMFC0rQ/T5pZjs/lQemtg92MnxnEDi5nhuvDwM4Q8eqCTOXc4BCE7iyIHv+B7rx+0x99ytMh5BSIIGyWTfgTot/AjGVm5aRKJSRFgPBm9N comment with whitespace", + "url": "http://localhost:3000/api/v1/repos/unknwon/project_x/keys/1", + "title": "local", + "created_at": "2015-11-18T15:05:43-05:00", + "read_only": true + }""" + self.deploy_key_json_list = """[{ + "id": 1, + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUbmwBOG5vI8qNCztby5LDc9ozwTuwsqf+1fpuHjT9iQ2Lu9nlKHQJcPSgdrYAcc+88K6o74ayhTAjfajKxkIHnbzZFjidoVZSQDhX5qvl93jvY/Uz390qky0sweW+fspm8pRJL+ofE3QEN5AXAuycq1tgsRT32XC+Ta82Xyv8b3xW+pWbsZzYCzUsZXDe/xWxg1rndXh2BIrmcYf9BMiv9ZJIojJXfuLCeRXl550tDzaMFC0rQ/T5pZjs/lQemtg92MnxnEDi5nhuvDwM4Q8eqCTOXc4BCE7iyIHv+B7rx+0x99ytMh5BSIIGyWTfgTot/AjGVm5aRKJSRFgPBm9N comment with whitespace", + "url": "http://localhost:3000/api/v1/repos/unknwon/project_x/keys/1", + "title": "local", + "created_at": "2015-11-18T15:05:43-05:00", + "read_only": true + },{ + "id": 2, + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUbmwBOG5vI8qNCztby5LDc9ozwTuwsqf+1fpuHjT9iQ2Lu9nlKHQJcPSgdrYAcc+88K6o74ayhTAjfajKxkIHnbzZFjidoVZSQDhX5qvl93jvY/Uz390qky0sweW+fspm8pRJL+ofE3QEN5AXAuycq1tgsRT32XC+Ta82Xyv8b3xW+pWbsZzYCzUsZXDe/xWxg1rndXh2BIrmcYf9BMiv9ZJIojJXfuLCeRXl550tDzaMFC0rQ/T5pZjs/lQemtg92MnxnEDi5nhuvDwM4Q8eqCTOXc4BCE7iyIHv+B7rx+0x99ytMh5BSIIGyWTfgTot/AjGVm5aRKJSRFgPBm9N comment with whitespace", + "url": "http://localhost:3000/api/v1/repos/unknwon/project_x/keys/1", + "title": "local", + "created_at": "2015-11-18T15:05:43-05:00", + "read_only": true + }]""" self.expected_repo = gogs_client.GogsRepo.from_json(json.loads(self.repo_json_str)) self.expected_user = gogs_client.GogsUser.from_json(json.loads(self.user_json_str)) self.expected_hook = gogs_client.GogsRepo.Hook.from_json(json.loads(self.hook_json_str)) self.expected_org = gogs_client.GogsOrg.from_json(json.loads(self.org_json_str)) self.expected_team = gogs_client.GogsOrg.Team.from_json(json.loads(self.team_json_str)) + self.expected_key = gogs_client.GogsRepo.DeployKey.from_json(json.loads(self.deploy_key_json_str)) self.token = gogs_client.Token.from_json(json.loads(self.token_json_str)) @responses.activate @@ -374,7 +398,7 @@ def callback(request): @responses.activate def test_list_hooks(self): uri = self.path("/repos/username/repo1/hooks") - responses.add(responses.GET, uri, body=self.hooks_list_json_str, status=20) + responses.add(responses.GET, uri, body=self.hooks_list_json_str, status=200) hooks = self.client.get_repo_hooks(self.token, "username", "repo1") self.assertEqual(len(hooks), 2) self.assert_hooks_equals(hooks[0], self.expected_hook) @@ -444,6 +468,38 @@ def test_remove_repo_from_team(self): resp = self.client.remove_repo_from_team(self.token, "test_team", "repo_name") self.assertEqual(resp.status_code, 204) + @responses.activate + def test_list_deploy_keys(self): + uri = self.path("/repos/username/repo1/keys") + responses.add(responses.GET, uri, body=self.deploy_key_json_list, status=200) + keys = self.client.list_deploy_keys(self.token, "username", "repo1") + self.assertEqual(len(keys), 2) + self.assert_keys_equals(keys[0], self.expected_key) + + @responses.activate + def test_delete_deploy_keys(self): + uri = self.path("/repos/username/repo1/keys/1") + responses.add(responses.DELETE, uri, status=204) + key = self.client.delete_deploy_key(self.token, "username", "repo1", 1) + self.assertEqual(key.status_code, 204) + + @responses.activate + def test_get_deploy_key(self): + uri = self.path("/repos/username/repo1/keys/1") + responses.add(responses.GET, uri, body=self.deploy_key_json_str) + key = self.client.get_deploy_key(self.token, "username", "repo1", 1) + self.assert_keys_equals(key, self.expected_key) + + @responses.activate + def test_add_deploy_key(self): + uri = self.path("/repos/username/repo1/keys") + responses.add(responses.POST, uri, body=self.deploy_key_json_str) + key_title = "My key title" + key_content = "My key content" + key = self.client.add_deploy_key(self.token, "username", "repo1", key_title, key_content) + self.assert_keys_equals(key, self.expected_key) + + # helper methods @staticmethod @@ -509,5 +565,13 @@ def assert_team_equals(self, team, expected): self.assertEqual(team.description, expected.description) self.assertEqual(team.permission, expected.permission) + def assert_keys_equals(self, key, expected): + self.assertEqual(key.key_id, expected.key_id) + self.assertEqual(key.title, expected.title) + self.assertEqual(key.url, expected.url) + self.assertEqual(key.key, expected.key) + self.assertEqual(key.read_only, expected.read_only) + self.assertEqual(key.created_at, expected.created_at) + if __name__ == "__main__": unittest.main() From f822715bcade9174f0b762554835f14ea4f372fd Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Wed, 5 Apr 2017 18:46:55 +0300 Subject: [PATCH 17/33] Added migrate_repo --- gogs_client/interface.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/gogs_client/interface.py b/gogs_client/interface.py index 61c8c8b..674f112 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -191,6 +191,43 @@ def delete_repo(self, auth, username, repo_name): path = "/repos/{u}/{r}".format(u=username, r=repo_name) self._check_ok(self._delete(path, auth=auth)) + def migrate_repo(self, auth, clone_addr, + uid, repo_name, auth_username=None, auth_password=None, + mirror=False, private=False, description=None): + """ + Migrate a repository from other Git hosting sources for the authenticated user. + + :param str clone_addr: Remote Git address (HTTP/HTTPS URL or local path) + :param str auth_username: Authorization username + :param str auth_password: Authorization password + :param str uid: User ID who takes ownership of this repository + :param str repo_name: Repository name + :param bool mirror: Repository will be a mirror. Default is false + :param bool private: Repository will be private. Default is false + :param str descriptrion: Repository description + :return: a representation of the migrated repository + :rtype: GogsRepo + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + # "auth_username": auth_username, + # "auth_password": auth_password, + + data = { + "clone_addr": clone_addr, + "uid": uid, + "repo_name": repo_name, + "auth_username": auth_username, + "auth_password": auth_password, + "mirror": mirror, + "private": private, + "description": description, + } + data = {k: v for (k, v) in data.items() if v is not None} + url = "/repos/migrate" + response = self._post(url, auth=auth, data=data) + return GogsRepo.from_json(self._check_ok(response).json()) + def create_user(self, auth, login_name, username, email, password, send_notify=False): """ Creates a new user, and returns the created user. From f899c406c2c03190d9c3a8b1e92ce83cdb89873c Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 09:29:51 +0300 Subject: [PATCH 18/33] Fixed uid type to int in migrate_repo docs. --- gogs_client/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gogs_client/interface.py b/gogs_client/interface.py index 674f112..d30815d 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -200,7 +200,7 @@ def migrate_repo(self, auth, clone_addr, :param str clone_addr: Remote Git address (HTTP/HTTPS URL or local path) :param str auth_username: Authorization username :param str auth_password: Authorization password - :param str uid: User ID who takes ownership of this repository + :param int uid: User ID who takes ownership of this repository :param str repo_name: Repository name :param bool mirror: Repository will be a mirror. Default is false :param bool private: Repository will be private. Default is false From 0b8fd171ee1370e9fcd62690c2a8cc1bc59960a6 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 09:30:03 +0300 Subject: [PATCH 19/33] First drone file --- .drone.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..5fe7f43 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,5 @@ +pipeline: + build: + image: python:alpine + commands: + - python tests/interface_test.py \ No newline at end of file From c101f86009e1f9a9a796b0b757abcc1c267b6b8f Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 10:17:02 +0300 Subject: [PATCH 20/33] added all feature branches to drone --- .drone.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 5fe7f43..dcc2f8d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,5 +1,14 @@ pipeline: build: - image: python:alpine + image: python:${PYTHON_VERSION}:alpine commands: - - python tests/interface_test.py \ No newline at end of file + - python tests/interface_test.py + +matrix: + PYTHON_VERSION: + - 2.7 + - 3.3 + - 3.4 + - 3.5 + +branches: [ master, feature/* ] \ No newline at end of file From 5216f440aa82001d3c3d5cd88a568d303f47db50 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 10:22:07 +0300 Subject: [PATCH 21/33] test file for drone --- test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test.txt diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..e69de29 From 3f8c2050325e692877bf3c876d848e000a69c0d8 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 14:22:53 +0300 Subject: [PATCH 22/33] testing drone --- test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test.txt b/test.txt index e69de29..5f94403 100644 --- a/test.txt +++ b/test.txt @@ -0,0 +1 @@ +asdasdas From f942de35ccf84d2439962658fcd168e428f606bc Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 14:25:49 +0300 Subject: [PATCH 23/33] testing drone --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index dcc2f8d..bdf6820 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,6 +1,6 @@ pipeline: build: - image: python:${PYTHON_VERSION}:alpine + image: python:${PYTHON_VERSION}-alpine commands: - python tests/interface_test.py From 042c6f7072b61f7fd06cc421527ca53cabc1a908 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 14:27:22 +0300 Subject: [PATCH 24/33] drone - install req --- .drone.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.drone.yml b/.drone.yml index bdf6820..50099e0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,6 +2,7 @@ pipeline: build: image: python:${PYTHON_VERSION}-alpine commands: + - pip install -r requirements.txt - python tests/interface_test.py matrix: From 93889963edaa546b886b5b5f0910df1478bf2099 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 14:28:52 +0300 Subject: [PATCH 25/33] drone - install test req --- .drone.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.drone.yml b/.drone.yml index 50099e0..bacf23f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,6 +3,7 @@ pipeline: image: python:${PYTHON_VERSION}-alpine commands: - pip install -r requirements.txt + - pip install -r test_requirements.txt - python tests/interface_test.py matrix: From 4efbf3f0d97c95d18ae1346fe0d1747b9ff7dc47 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Mon, 10 Apr 2017 14:32:36 +0300 Subject: [PATCH 26/33] drone - notify via slack --- .drone.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index bacf23f..984f3b0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,6 +6,18 @@ pipeline: - pip install -r test_requirements.txt - python tests/interface_test.py + + slack: + image: plugins/slack + webhook: https://hooks.slack.com/services/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 + channel: python-gogs-client + template: > + {{#success build.status}} + build {{build.number}} succeeded. Good job. + {{else}} + build {{build.number}} failed. Fix me please. + {{/success}} + matrix: PYTHON_VERSION: - 2.7 @@ -13,4 +25,5 @@ matrix: - 3.4 - 3.5 -branches: [ master, feature/* ] \ No newline at end of file +branches: [ master, feature/* ] + From dbce6104edd490f8d8e477a656ed3a46fae7c592 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 11 Apr 2017 10:10:37 +0300 Subject: [PATCH 27/33] List user repos --- gogs_client/interface.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gogs_client/interface.py b/gogs_client/interface.py index d30815d..f6bc134 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -178,6 +178,23 @@ def get_repo(self, auth, username, repo_name): response = self._check_ok(self._get(path, auth=auth)) return GogsRepo.from_json(response.json()) + def get_user_repos(self, auth, username): + """ + Returns the repositories owned by + the user with username ``username``. + + :param auth.Authentication auth: authentication object + :param str username: username of owner of repository + :return: a list of repositories + :rtype: List[GogsRepo] + :raises NetworkFailure: if there is an error communicating with the server + :raises ApiFailure: if the request cannot be serviced + """ + path = "/users/{u}/repos".format(u=username) + response = self._check_ok(self._get(path, auth=auth)) + print response + return [GogsRepo.from_json(repo_json) for repo_json in response.json()] + def delete_repo(self, auth, username, repo_name): """ Deletes the repository with name ``repo_name`` owned by the user with username ``username``. From 748bc7c76309d6e5d35fe593fa204976b376a87a Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 11 Apr 2017 10:10:50 +0300 Subject: [PATCH 28/33] Test list user repos --- tests/interface_test.py | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/interface_test.py b/tests/interface_test.py index e8b6522..d83d287 100644 --- a/tests/interface_test.py +++ b/tests/interface_test.py @@ -36,6 +36,47 @@ def setUp(self): "pull": true } }""" + self.repos_list_json_str = """[{ + "id": 27, + "owner": { + "id": 1, + "username": "unknwon", + "full_name": "", + "email": "u@gogs.io", + "avatar_url": "/avatars/1" + }, + "full_name": "unknwon/Hello-World", + "private": false, + "fork": false, + "html_url": "http://localhost:3000/unknwon/Hello-World", + "clone_url": "http://localhost:3000/unknwon/hello-world.git", + "ssh_url": "jiahuachen@localhost:unknwon/hello-world.git", + "permissions": { + "admin": true, + "push": true, + "pull": true + } + },{ + "id": 28, + "owner": { + "id": 1, + "username": "unknwon", + "full_name": "", + "email": "u@gogs.io", + "avatar_url": "/avatars/1" + }, + "full_name": "unknwon/Hello-World-Again", + "private": false, + "fork": false, + "html_url": "http://localhost:3000/unknwon/Hello-World-Again", + "clone_url": "http://localhost:3000/unknwon/hello-world-again.git", + "ssh_url": "jiahuachen@localhost:unknwon/hello-world-again.git", + "permissions": { + "admin": true, + "push": true, + "pull": true + } + }]""" self.user_json_str = """{ "id": 1, "username": "unknwon", @@ -183,6 +224,15 @@ def test_get_repo1(self): last_call = responses.calls[1] self.assertEqual(last_call.request.url, self.path_with_token(uri2)) + @responses.activate + def test_get_user_repos(self): + uri = self.path("/users/username/repos") + responses.add(responses.GET, uri, body=self.repos_list_json_str, status=200) + repos = self.client.get_user_repos(self.token, "username") + self.assertEqual(len(repos), 2) + self.assert_repos_equal(repos[0], self.expected_repo) + + @responses.activate def test_delete_repo1(self): uri1 = self.path("/repos/username/repo1") From 5d74675e16d197b3528b34f2c5ba567ffda73345 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 11 Apr 2017 10:12:30 +0300 Subject: [PATCH 29/33] Remove custom template from slack notify --- .drone.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.drone.yml b/.drone.yml index 984f3b0..170607d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,12 +11,6 @@ pipeline: image: plugins/slack webhook: https://hooks.slack.com/services/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 channel: python-gogs-client - template: > - {{#success build.status}} - build {{build.number}} succeeded. Good job. - {{else}} - build {{build.number}} failed. Fix me please. - {{/success}} matrix: PYTHON_VERSION: From a31513a5c0f072b1892c44fda8a1cab5ee485d5e Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 11 Apr 2017 10:14:44 +0300 Subject: [PATCH 30/33] Remove print statement --- gogs_client/interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gogs_client/interface.py b/gogs_client/interface.py index f6bc134..e4f2d53 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -192,7 +192,6 @@ def get_user_repos(self, auth, username): """ path = "/users/{u}/repos".format(u=username) response = self._check_ok(self._get(path, auth=auth)) - print response return [GogsRepo.from_json(repo_json) for repo_json in response.json()] def delete_repo(self, auth, username, repo_name): From 745a009d9746654bfeefe898e70a657cc104ca7b Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 11 Apr 2017 10:42:59 +0300 Subject: [PATCH 31/33] Added secrets in drone.yml --- .drone.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index 170607d..b4e6c17 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,8 +9,8 @@ pipeline: slack: image: plugins/slack - webhook: https://hooks.slack.com/services/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 - channel: python-gogs-client + webhook: ${WEBHOOK} + channel: ${CHANNEL} matrix: PYTHON_VERSION: @@ -19,5 +19,4 @@ matrix: - 3.4 - 3.5 -branches: [ master, feature/* ] - +branches: [ master, feature/* ] \ No newline at end of file From 12db57a5a8bac6029ff360135e458d1842856fcb Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Tue, 11 Apr 2017 10:59:34 +0300 Subject: [PATCH 32/33] databus-systems drone file to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e9cfa15..e63b4a4 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,5 @@ ENV/ # IDE files .idea/ +# DRONE file +.drone.yml From 05b4291d3b07a3d5c0333a1dd1f35fc489fb6631 Mon Sep 17 00:00:00 2001 From: Costin Bleotu Date: Wed, 12 Apr 2017 10:04:44 +0300 Subject: [PATCH 33/33] Fixed changes request --- .drone.yml | 22 ---------------------- gogs_client/entities.py | 16 ++++++++-------- gogs_client/interface.py | 24 +++++------------------- test.txt | 1 - tests/interface_test.py | 10 +++++----- 5 files changed, 18 insertions(+), 55 deletions(-) delete mode 100644 .drone.yml delete mode 100644 test.txt diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index b4e6c17..0000000 --- a/.drone.yml +++ /dev/null @@ -1,22 +0,0 @@ -pipeline: - build: - image: python:${PYTHON_VERSION}-alpine - commands: - - pip install -r requirements.txt - - pip install -r test_requirements.txt - - python tests/interface_test.py - - - slack: - image: plugins/slack - webhook: ${WEBHOOK} - channel: ${CHANNEL} - -matrix: - PYTHON_VERSION: - - 2.7 - - 3.3 - - 3.4 - - 3.5 - -branches: [ master, feature/* ] \ No newline at end of file diff --git a/gogs_client/entities.py b/gogs_client/entities.py index 6c16ddd..6f5e191 100644 --- a/gogs_client/entities.py +++ b/gogs_client/entities.py @@ -291,16 +291,16 @@ def hook_type(self): @property def events(self): """ - The events that fires the hook + The events that fire the hook - :rtype: list of strs + :rtype: List[str] """ return self._events @property def active(self): """ - State of the hook + Whether the hook is active :rtype: bool """ @@ -397,7 +397,7 @@ def created_at(self): @property def read_only(self): """ - Is the key read only? + Whether key is read-only. :rtype: bool """ return self._read_only @@ -454,7 +454,7 @@ def username(self): """ Organization's username - :rtype: int + :rtype: str """ return self._username @@ -463,7 +463,7 @@ def full_name(self): """ Organization's full name - :rtype: int + :rtype: str """ return self._full_name @@ -544,7 +544,7 @@ def name(self): """ Team name - :rtype: int + :rtype: str """ return self._name @@ -553,7 +553,7 @@ def description(self): """ Description to the team - :rtype: int + :rtype: str """ return self._description diff --git a/gogs_client/interface.py b/gogs_client/interface.py index e4f2d53..4b3567a 100644 --- a/gogs_client/interface.py +++ b/gogs_client/interface.py @@ -420,7 +420,6 @@ def delete_hook(self, auth, username, repo_name, hook_id): """ path = "/repos/{u}/{r}/hooks/{i}".format(u=username, r=repo_name, i=hook_id) response = self._check_ok(self._delete(path, auth=auth)) - return response def create_organization(self, auth, username, org_name, full_name=None, avatar_url=None, description=None, website=None, location=None): """ @@ -457,7 +456,7 @@ def create_organization_team(self, auth, org_name, name, description=None, permi :param auth.Authentication auth: authentication object, must be admin-level :param str org_name: [Required] Organization user name - :param str team_name: Full name of the team + :param str name: Full name of the team :param str description: Description to the team :param str permission: Team permission, can be read, write or admin, default is read :return: a representation of the created team @@ -484,14 +483,11 @@ def add_team_membership(self, auth, team_id, username): :param auth.Authentication auth: authentication object, must be admin-level :param str team_id: [Required] Id of the team :param str username: Username of the user to be added to team - :return: status code of the request - :rtype: str :raises NetworkFailure: if there is an error communicating with the server :raises ApiFailure: if the request cannot be serviced """ url = "/admin/teams/{t}/members/{u}".format(t=team_id, u=username) - response = self._put(url, auth=auth) - return self._check_ok(response) + response = self._check_ok(self._put(url, auth=auth)) def remove_team_membership(self, auth, team_id, username): """ @@ -500,14 +496,11 @@ def remove_team_membership(self, auth, team_id, username): :param auth.Authentication auth: authentication object, must be admin-level :param str team_id: [Required] Id of the team :param str username: Username of the user to be removed from the team - :return: status code of the request - :rtype: str :raises NetworkFailure: if there is an error communicating with the server :raises ApiFailure: if the request cannot be serviced """ url = "/admin/teams/{t}/members/{u}".format(t=team_id, u=username) - response = self._delete(url, auth=auth) - return self._check_ok(response) + response = self._check_ok(self._delete(url, auth=auth)) def add_repo_to_team(self, auth, team_id, repo_name): """ @@ -516,15 +509,11 @@ def add_repo_to_team(self, auth, team_id, repo_name): :param auth.Authentication auth: authentication object, must be admin-level :param str team_id: [Required] Id of the team :param str repo_name: Name of the repo to be added to the team - :return: status code of the request - :rtype: str :raises NetworkFailure: if there is an error communicating with the server :raises ApiFailure: if the request cannot be serviced """ url = "/admin/teams/{t}/repos/{r}".format(t=team_id, r=repo_name) - response = self._put(url, auth=auth) - return self._check_ok(response) - + response = self._check_ok(self._put(url, auth=auth)) def remove_repo_from_team(self, auth, team_id, repo_name): """ @@ -533,14 +522,11 @@ def remove_repo_from_team(self, auth, team_id, repo_name): :param auth.Authentication auth: authentication object, must be admin-level :param str team_id: [Required] Id of the team :param str repo_name: Name of the repo to be removed from the team - :return: status code of the request - :rtype: str :raises NetworkFailure: if there is an error communicating with the server :raises ApiFailure: if the request cannot be serviced """ url = "/admin/teams/{t}/repos/{r}".format(t=team_id, r=repo_name) - response = self._delete(url, auth=auth) - return self._check_ok(response) + response = self._check_ok(self._delete(url, auth=auth)) def list_deploy_keys(self, auth, username, repo_name): """ diff --git a/test.txt b/test.txt deleted file mode 100644 index 5f94403..0000000 --- a/test.txt +++ /dev/null @@ -1 +0,0 @@ -asdasdas diff --git a/tests/interface_test.py b/tests/interface_test.py index d83d287..a0cbdab 100644 --- a/tests/interface_test.py +++ b/tests/interface_test.py @@ -458,7 +458,7 @@ def test_delete_hook(self): uri = self.path("/repos/username/repo1/hooks/4") responses.add(responses.DELETE, uri, status=204) hook = self.client.delete_hook(self.token, "username", "repo1", 4) - self.assertEqual(hook.status_code, 204) + self.assertEqual(hook, None) @responses.activate def test_create_organization(self): @@ -495,28 +495,28 @@ def test_add_team_membership(self): uri = self.path("/admin/teams/team/members/username") responses.add(responses.PUT, uri, status=204) resp = self.client.add_team_membership(self.token, "team", "username") - self.assertEqual(resp.status_code, 204) + self.assertEqual(resp, None) @responses.activate def test_remove_team_membership(self): uri = self.path("/admin/teams/team/members/username") responses.add(responses.DELETE, uri, status=204) resp = self.client.remove_team_membership(self.token, "team", "username") - self.assertEqual(resp.status_code, 204) + self.assertEqual(resp, None) @responses.activate def test_add_repo_to_team(self): uri = self.path("/admin/teams/test_team/repos/repo_name") responses.add(responses.PUT, uri, status=204) resp = self.client.add_repo_to_team(self.token, "test_team", "repo_name") - self.assertEqual(resp.status_code, 204) + self.assertEqual(resp, None) @responses.activate def test_remove_repo_from_team(self): uri = self.path("/admin/teams/test_team/repos/repo_name") responses.add(responses.DELETE, uri, status=204) resp = self.client.remove_repo_from_team(self.token, "test_team", "repo_name") - self.assertEqual(resp.status_code, 204) + self.assertEqual(resp, None) @responses.activate def test_list_deploy_keys(self):