From 28d751811ffda45ff0b1c35e0599b655f3a5a68b Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sun, 21 Feb 2021 12:01:33 +0100 Subject: [PATCH 1/4] feat(objects): add Release Links API support --- gitlab/tests/conftest.py | 10 ++ gitlab/tests/objects/test_releases.py | 131 ++++++++++++++++++++++++++ gitlab/v4/objects/__init__.py | 1 + gitlab/v4/objects/projects.py | 3 +- gitlab/v4/objects/releases.py | 36 +++++++ gitlab/v4/objects/tags.py | 13 --- 6 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 gitlab/tests/objects/test_releases.py create mode 100644 gitlab/v4/objects/releases.py diff --git a/gitlab/tests/conftest.py b/gitlab/tests/conftest.py index 98d97ae6e..fc8312f34 100644 --- a/gitlab/tests/conftest.py +++ b/gitlab/tests/conftest.py @@ -37,6 +37,11 @@ def default_config(tmpdir): return str(config_path) +@pytest.fixture +def tag_name(): + return "v1.0.0" + + @pytest.fixture def group(gl): return gl.groups.get(1, lazy=True) @@ -47,6 +52,11 @@ def project(gl): return gl.projects.get(1, lazy=True) +@pytest.fixture +def release(project, tag_name): + return project.releases.get(tag_name, lazy=True) + + @pytest.fixture def user(gl): return gl.users.get(1, lazy=True) diff --git a/gitlab/tests/objects/test_releases.py b/gitlab/tests/objects/test_releases.py new file mode 100644 index 000000000..6c38a7c48 --- /dev/null +++ b/gitlab/tests/objects/test_releases.py @@ -0,0 +1,131 @@ +""" +GitLab API: +https://docs.gitlab.com/ee/api/releases/index.html +https://docs.gitlab.com/ee/api/releases/links.html +""" +import re + +import pytest +import responses + +from gitlab.v4.objects import ProjectReleaseLink + +encoded_tag_name = "v1%2E0%2E0" +link_name = "hello-world" +link_url = "https://gitlab.example.com/group/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64" +direct_url = f"https://gitlab.example.com/group/hello/-/releases/{encoded_tag_name}/downloads/hello-world" +new_link_type = "package" +link_content = { + "id": 2, + "name": link_name, + "url": link_url, + "direct_asset_url": direct_url, + "external": False, + "link_type": "other", +} + +links_url = re.compile( + rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links" +) +link_id_url = re.compile( + rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links/1" +) + + +@pytest.fixture +def resp_list_links(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=links_url, + json=[link_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_link(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=link_id_url, + json=link_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_link(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url=links_url, + json=link_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_update_link(): + updated_content = dict(link_content) + updated_content["link_type"] = new_link_type + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.PUT, + url=link_id_url, + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_link(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url=link_id_url, + json=link_content, + content_type="application/json", + status=204, + ) + yield rsps + + +def test_list_release_links(release, resp_list_links): + links = release.links.list() + assert isinstance(links, list) + assert isinstance(links[0], ProjectReleaseLink) + assert links[0].url == link_url + + +def test_get_release_link(release, resp_get_link): + link = release.links.get(1) + assert isinstance(link, ProjectReleaseLink) + assert link.url == link_url + + +def test_create_release_link(release, resp_create_link): + link = release.links.create({"url": link_url, "name": link_name}) + assert isinstance(link, ProjectReleaseLink) + assert link.url == link_url + + +def test_update_release_link(release, resp_update_link): + link = release.links.get(1, lazy=True) + link.link_type = new_link_type + link.save() + assert link.link_type == new_link_type + + +def test_delete_release_link(release, resp_delete_link): + link = release.links.get(1, lazy=True) + link.delete() diff --git a/gitlab/v4/objects/__init__.py b/gitlab/v4/objects/__init__.py index 9f91f5348..8a2ed7c37 100644 --- a/gitlab/v4/objects/__init__.py +++ b/gitlab/v4/objects/__init__.py @@ -56,6 +56,7 @@ from .pipelines import * from .projects import * from .push_rules import * +from .releases import * from .runners import * from .services import * from .settings import * diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index 722b9ea9e..b354af9ac 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -33,6 +33,7 @@ from .pages import ProjectPagesDomainManager from .pipelines import ProjectPipelineManager, ProjectPipelineScheduleManager from .push_rules import ProjectPushRulesManager +from .releases import ProjectReleaseManager from .runners import ProjectRunnerManager from .services import ProjectServiceManager from .snippets import ProjectSnippetManager @@ -40,7 +41,7 @@ ProjectAdditionalStatisticsManager, ProjectIssuesStatisticsManager, ) -from .tags import ProjectProtectedTagManager, ProjectReleaseManager, ProjectTagManager +from .tags import ProjectProtectedTagManager, ProjectTagManager from .triggers import ProjectTriggerManager from .users import ProjectUserManager from .variables import ProjectVariableManager diff --git a/gitlab/v4/objects/releases.py b/gitlab/v4/objects/releases.py new file mode 100644 index 000000000..d9112e4cc --- /dev/null +++ b/gitlab/v4/objects/releases.py @@ -0,0 +1,36 @@ +from gitlab import cli +from gitlab import exceptions as exc +from gitlab.base import * # noqa +from gitlab.mixins import * # noqa + + +__all__ = [ + "ProjectRelease", + "ProjectReleaseManager", + "ProjectReleaseLink", + "ProjectReleaseLinkManager", +] + + +class ProjectRelease(RESTObject): + _id_attr = "tag_name" + _managers = (("links", "ProjectReleaseLinkManager"),) + + +class ProjectReleaseManager(NoUpdateMixin, RESTManager): + _path = "/projects/%(project_id)s/releases" + _obj_cls = ProjectRelease + _from_parent_attrs = {"project_id": "id"} + _create_attrs = (("name", "tag_name", "description"), ("ref", "assets")) + + +class ProjectReleaseLink(RESTObject, ObjectDeleteMixin, SaveMixin): + pass + + +class ProjectReleaseLinkManager(CRUDMixin, RESTManager): + _path = "/projects/%(project_id)s/releases/%(tag_name)s/assets/links" + _obj_cls = ProjectReleaseLink + _from_parent_attrs = {"project_id": "project_id", "tag_name": "tag_name"} + _create_attrs = (("name", "url"), ("filepath", "link_type")) + _update_attrs = ((), ("name", "url", "filepath", "link_type")) diff --git a/gitlab/v4/objects/tags.py b/gitlab/v4/objects/tags.py index c4d60db05..1f333c566 100644 --- a/gitlab/v4/objects/tags.py +++ b/gitlab/v4/objects/tags.py @@ -9,8 +9,6 @@ "ProjectTagManager", "ProjectProtectedTag", "ProjectProtectedTagManager", - "ProjectRelease", - "ProjectReleaseManager", ] @@ -71,14 +69,3 @@ class ProjectProtectedTagManager(NoUpdateMixin, RESTManager): _obj_cls = ProjectProtectedTag _from_parent_attrs = {"project_id": "id"} _create_attrs = (("name",), ("create_access_level",)) - - -class ProjectRelease(RESTObject): - _id_attr = "tag_name" - - -class ProjectReleaseManager(NoUpdateMixin, RESTManager): - _path = "/projects/%(project_id)s/releases" - _obj_cls = ProjectRelease - _from_parent_attrs = {"project_id": "id"} - _create_attrs = (("name", "tag_name", "description"), ("ref", "assets")) From 958a6aa83ead3fb6be6ec61bdd894ad78346e7bd Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sun, 21 Feb 2021 12:04:14 +0100 Subject: [PATCH 2/4] chore(objects): make Project refreshable Helps getting the real state of the project from the server. --- gitlab/v4/objects/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index b354af9ac..320e511ce 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -87,7 +87,7 @@ class GroupProjectManager(ListMixin, RESTManager): ) -class Project(SaveMixin, ObjectDeleteMixin, RESTObject): +class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "path" _managers = ( ("accessrequests", "ProjectAccessRequestManager"), From ab2a1c816d83e9e308c0c9c7abf1503438b0b3be Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sun, 21 Feb 2021 12:25:55 +0100 Subject: [PATCH 3/4] test(api): add functional test for release links API --- tools/functional/api/test_projects.py | 36 +-------------------------- tools/functional/api/test_releases.py | 36 +++++++++++++++++++++++++++ tools/functional/conftest.py | 33 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 35 deletions(-) create mode 100644 tools/functional/api/test_releases.py diff --git a/tools/functional/api/test_projects.py b/tools/functional/api/test_projects.py index 945a6ec3f..404f89dce 100644 --- a/tools/functional/api/test_projects.py +++ b/tools/functional/api/test_projects.py @@ -197,32 +197,6 @@ def test_project_protected_branches(project): assert len(project.protectedbranches.list()) == 0 -def test_project_releases(gl): - 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 = project.releases.create( - { - "name": release_name, - "tag_name": release_tag_name, - "description": release_description, - "ref": "master", - } - ) - assert len(project.releases.list()) == 1 - assert project.releases.get(release_tag_name) - assert release.name == release_name - assert release.tag_name == release_tag_name - assert release.description == release_description - - project.releases.delete(release_tag_name) - assert len(project.releases.list()) == 0 - project.delete() - - def test_project_remote_mirrors(project): mirror_url = "http://gitlab.test/root/mirror.git" @@ -260,15 +234,7 @@ def test_project_stars(project): assert project.star_count == 0 -def test_project_tags(project): - project.files.create( - { - "file_path": "README", - "branch": "master", - "content": "Initial content", - "commit_message": "Initial commit", - } - ) +def test_project_tags(project, project_file): tag = project.tags.create({"tag_name": "v1.0", "ref": "master"}) assert len(project.tags.list()) == 1 diff --git a/tools/functional/api/test_releases.py b/tools/functional/api/test_releases.py new file mode 100644 index 000000000..55f7920f2 --- /dev/null +++ b/tools/functional/api/test_releases.py @@ -0,0 +1,36 @@ +release_name = "Demo Release" +release_tag_name = "v1.2.3" +release_description = "release notes go here" + +link_data = {"url": "https://example.com", "name": "link_name"} + + +def test_create_project_release(project, project_file): + project.refresh() # Gets us the current default branch + release = project.releases.create( + { + "name": release_name, + "tag_name": release_tag_name, + "description": release_description, + "ref": project.default_branch, + } + ) + + assert len(project.releases.list()) == 1 + assert project.releases.get(release_tag_name) + assert release.name == release_name + assert release.tag_name == release_tag_name + assert release.description == release_description + + +def test_delete_project_release(project, release): + project.releases.delete(release.tag_name) + assert release not in project.releases.list() + + +def test_create_project_release_links(project, release): + link = release.links.create(link_data) + + release = project.releases.get(release.tag_name) + assert release.assets["links"][0]["url"] == link_data["url"] + assert release.assets["links"][0]["name"] == link_data["name"] diff --git a/tools/functional/conftest.py b/tools/functional/conftest.py index 675dba960..a0b14f9c2 100644 --- a/tools/functional/conftest.py +++ b/tools/functional/conftest.py @@ -196,6 +196,39 @@ def project(gl): print(f"Project already deleted: {e}") +@pytest.fixture(scope="module") +def project_file(project): + """File fixture for tests requiring a project with files and branches.""" + project_file = project.files.create( + { + "file_path": "README", + "branch": "master", + "content": "Initial content", + "commit_message": "Initial commit", + } + ) + + return project_file + + +@pytest.fixture(scope="function") +def release(project, project_file): + _id = uuid.uuid4().hex + name = f"test-release-{_id}" + + project.refresh() # Gets us the current default branch + release = project.releases.create( + { + "name": name, + "tag_name": _id, + "description": "description", + "ref": project.default_branch, + } + ) + + return release + + @pytest.fixture(scope="module") def user(gl): """User fixture for user API resource tests.""" From 36d65f03db253d710938c2d827c1124c94a40506 Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sun, 21 Feb 2021 12:45:12 +0100 Subject: [PATCH 4/4] docs(api): add release links API docs --- docs/api-objects.rst | 1 + docs/gl_objects/projects.rst | 33 ---------------- docs/gl_objects/releases.rst | 77 ++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 33 deletions(-) create mode 100644 docs/gl_objects/releases.rst diff --git a/docs/api-objects.rst b/docs/api-objects.rst index 8221f63b8..5bcbe24ff 100644 --- a/docs/api-objects.rst +++ b/docs/api-objects.rst @@ -37,6 +37,7 @@ API examples gl_objects/pipelines_and_jobs gl_objects/projects gl_objects/protected_branches + gl_objects/releases gl_objects/runners gl_objects/remote_mirrors gl_objects/repositories diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index e483a3253..e61bb6a53 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -702,39 +702,6 @@ Delete project push rules:: pr.delete() -Project releases -================ - -Reference ---------- - -* v4 API: - - + :class:`gitlab.v4.objects.ProjectRelease` - + :class:`gitlab.v4.objects.ProjectReleaseManager` - + :attr:`gitlab.v4.objects.Project.releases` - -* Gitlab API: https://docs.gitlab.com/ee/api/releases/index.html - -Examples --------- - -Get a list of releases from a project:: - - release = project.releases.list() - -Get a single release:: - - release = project.releases.get('v1.2.3') - -Create a release for a project tag:: - - release = project.releases.create({'name':'Demo Release', 'tag_name':'v1.2.3', 'description':'release notes go here'}) - -Delete a release:: - - release = p.releases.delete('v1.2.3') - Project protected tags ====================== diff --git a/docs/gl_objects/releases.rst b/docs/gl_objects/releases.rst new file mode 100644 index 000000000..38138570c --- /dev/null +++ b/docs/gl_objects/releases.rst @@ -0,0 +1,77 @@ +######## +Releases +######## + +Project releases +================ + +Reference +--------- + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectRelease` + + :class:`gitlab.v4.objects.ProjectReleaseManager` + + :attr:`gitlab.v4.objects.Project.releases` + +* Gitlab API: https://docs.gitlab.com/ee/api/releases/index.html + +Examples +-------- + +Get a list of releases from a project:: + + release = project.releases.list() + +Get a single release:: + + release = project.releases.get('v1.2.3') + +Create a release for a project tag:: + + release = project.releases.create({'name':'Demo Release', 'tag_name':'v1.2.3', 'description':'release notes go here'}) + +Delete a release:: + + # via its tag name from project attributes + release = project.releases.delete('v1.2.3') + + # delete object directly + release.delete() + +Project release links +===================== + +Reference +--------- + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectReleaseLink` + + :class:`gitlab.v4.objects.ProjectReleaseLinkManager` + + :attr:`gitlab.v4.objects.ProjectRelease.links` + +* Gitlab API: https://docs.gitlab.com/ee/api/releases/links.html + +Examples +-------- + +Get a list of releases from a project:: + + links = release.links.list() + +Get a single release link:: + + link = release.links.get(1) + +Create a release link for a release:: + + link = release.links.create({"url": "https://example.com/asset", "name": "asset"}) + +Delete a release link:: + + # via its ID from release attributes + release.links.delete(1) + + # delete object directly + link.delete()