diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py index 50eb5ca..3357164 100644 --- a/cloudfoundry_client/v3/apps.py +++ b/cloudfoundry_client/v3/apps.py @@ -33,6 +33,10 @@ def list_droplets(self, application_guid: str, **kwargs) -> Pagination[Entity]: def get_manifest(self, application_guid: str) -> str: return self.client.get(url="%s%s/%s/manifest" % (self.target_endpoint, self.entity_uri, application_guid)).text + def list_packages(self, application_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/packages" % (self.entity_uri, application_guid) + return super(AppManager, self)._list(requested_path=uri, **kwargs) + def list_revisions(self, application_guid: str, **kwargs) -> Pagination[Entity]: uri: str = "%s/%s/revisions" % (self.entity_uri, application_guid) return super(AppManager, self)._list(requested_path=uri, **kwargs) diff --git a/cloudfoundry_client/v3/packages.py b/cloudfoundry_client/v3/packages.py index 9e8e3d3..cb554dd 100644 --- a/cloudfoundry_client/v3/packages.py +++ b/cloudfoundry_client/v3/packages.py @@ -1,16 +1,55 @@ -from typing import TYPE_CHECKING +from enum import Enum +from typing import TYPE_CHECKING, Any from cloudfoundry_client.common_objects import Pagination -from cloudfoundry_client.v3.entities import EntityManager, Entity +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToOneRelationship if TYPE_CHECKING: from cloudfoundry_client.client import CloudFoundryClient +class PackageType(Enum): + BITS = 'bits' + DOCKER = 'docker' + + class PackageManager(EntityManager[Entity]): def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): super(PackageManager, self).__init__(target_endpoint, client, "/v3/packages") + def create(self, + app_guid: str, + package_type: PackageType, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = { + "type": package_type.value, + "relationships": { + "app": ToOneRelationship(app_guid) + }, + } + self._metadata(data, meta_labels, meta_annotations) + return super(PackageManager, self)._create(data) + + def copy(self, + package_guid: str, + app_guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = { + "relationships": { + "app": ToOneRelationship(app_guid) + }, + } + self._metadata(data, meta_labels, meta_annotations) + url = EntityManager._get_url_with_encoded_params( + "%s%s" % (self.target_endpoint, self.entity_uri), + source_guid=package_guid + ) + return self._post(url, data=data) + def list_droplets(self, package_guid: str, **kwargs) -> Pagination[Entity]: uri: str = "%s/%s/droplets" % (self.entity_uri, package_guid) return super(PackageManager, self)._list(requested_path=uri, **kwargs) diff --git a/tests/fixtures/v3/apps/GET_{id}_packages_response.json b/tests/fixtures/v3/apps/GET_{id}_packages_response.json new file mode 100644 index 0000000..8c48599 --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_packages_response.json @@ -0,0 +1,57 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/apps/f2efe391-2b5b-4836-8518-ad93fa9ebf69/packages?states=READY&page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/apps/f2efe391-2b5b-4836-8518-ad93fa9ebf69/packages?states=READY&page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "752edab0-2147-4f58-9c25-cd72ad8c3561", + "type": "bits", + "data": { + "error": null, + "checksum": { + "type": "sha256", + "value": null + } + }, + "state": "READY", + "created_at": "2016-03-17T21:41:09Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "app": { + "data": { + "guid": "f2efe391-2b5b-4836-8518-ad93fa9ebf69" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/packages/752edab0-2147-4f58-9c25-cd72ad8c3561" + }, + "upload": { + "href": "https://api.example.org/v3/packages/752edab0-2147-4f58-9c25-cd72ad8c3561/upload", + "method": "POST" + }, + "download": { + "href": "https://api.example.org/v3/packages/752edab0-2147-4f58-9c25-cd72ad8c3561/download", + "method": "GET" + }, + "app": { + "href": "https://api.example.org/v3/apps/f2efe391-2b5b-4836-8518-ad93fa9ebf69" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} diff --git a/tests/fixtures/v3/packages/POST_response.json b/tests/fixtures/v3/packages/POST_response.json new file mode 100644 index 0000000..3e72e01 --- /dev/null +++ b/tests/fixtures/v3/packages/POST_response.json @@ -0,0 +1,41 @@ +{ + "guid": "44f7c078-0934-470f-9883-4fcddc5b8f13", + "type": "bits", + "data": { + "checksum": { + "type": "sha256", + "value": null + }, + "error": null + }, + "state": "PROCESSING_UPLOAD", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "app": { + "data": { + "guid": "1d3bf0ec-5806-43c4-b64e-8364dba1086a" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/packages/44f7c078-0934-470f-9883-4fcddc5b8f13" + }, + "upload": { + "href": "https://api.example.org/v3/packages/44f7c078-0934-470f-9883-4fcddc5b8f13/upload", + "method": "POST" + }, + "download": { + "href": "https://api.example.org/v3/packages/44f7c078-0934-470f-9883-4fcddc5b8f13/download", + "method": "GET" + }, + "app": { + "href": "https://api.example.org/v3/apps/1d3bf0ec-5806-43c4-b64e-8364dba1086a" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py index 67d0827..dbe8fb6 100644 --- a/tests/v3/test_apps.py +++ b/tests/v3/test_apps.py @@ -97,6 +97,22 @@ def test_get_env(self): self.assertIsInstance(env, JsonObject) self.assertEqual(env["application_env_json"]["VCAP_APPLICATION"]["limits"]["fds"], 16384) + def test_list_droplets(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id/droplets", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_droplets_response.json" + ) + droplets: list[dict] = [revision for revision in self.client.v3.apps.list_droplets("app_id")] + self.assertEqual(2, len(droplets)) + self.assertEqual(droplets[0]["state"], "STAGED") + + def test_list_packages(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id/packages", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_packages_response.json" + ) + packages: list[dict] = [package for package in self.client.v3.apps.list_packages("app_id")] + self.assertEqual(1, len(packages)) + self.assertEqual(packages[0]["type"], "bits") + def test_list_routes(self): self.client.get.return_value = self.mock_response( "/v3/apps/app_id/routes", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_routes_response.json" @@ -106,14 +122,6 @@ def test_list_routes(self): self.assertEqual(routes[0]["destinations"][0]["guid"], "385bf117-17f5-4689-8c5c-08c6cc821fed") - def test_list_droplets(self): - self.client.get.return_value = self.mock_response( - "/v3/apps/app_id/droplets", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_droplets_response.json" - ) - droplets: list[dict] = [revision for revision in self.client.v3.apps.list_droplets("app_id")] - self.assertEqual(2, len(droplets)) - self.assertEqual(droplets[0]["state"], "STAGED") - def test_get_include_space_and_org(self): self.client.get.return_value = self.mock_response( "/v3/apps/app_id?include=space.organization", diff --git a/tests/v3/test_packages.py b/tests/v3/test_packages.py index 0db7c46..e19e13c 100644 --- a/tests/v3/test_packages.py +++ b/tests/v3/test_packages.py @@ -3,6 +3,7 @@ from abstract_test_case import AbstractTestCase from cloudfoundry_client.v3.entities import Entity +from cloudfoundry_client.v3.packages import PackageType class TestPackages(unittest.TestCase, AbstractTestCase): @@ -13,6 +14,67 @@ def setUpClass(cls): def setUp(self): self.build_client() + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/packages", + HTTPStatus.OK, + None, + "v3", "packages", "POST_response.json" + ) + result = self.client.v3.packages.create( + app_guid="app-guid", + package_type=PackageType.BITS, + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "relationships": { + "app": { + "data": { + "guid": "app-guid" + } + } + }, + "type": "bits", + "metadata": {"labels": {"key": "value"}, "annotations": {"note": "detailed information"}} + }, + files=None, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_copy(self): + self.client.post.return_value = self.mock_response( + "/v3/packages?source_guid=package_id", + HTTPStatus.OK, + None, + "v3", "packages", "POST_response.json" + ) + result = self.client.v3.packages.copy( + package_guid="package_id", + app_guid="app-guid", + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "relationships": { + "app": { + "data": { + "guid": "app-guid" + } + } + }, + "metadata": {"labels": {"key": "value"}, "annotations": {"note": "detailed information"}} + }, + files=None, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + def test_list(self): self.client.get.return_value = self.mock_response( "/v3/packages", @@ -24,8 +86,8 @@ def test_list(self): self.client.get.assert_called_with(self.client.get.return_value.url) self.assertEqual(2, len(all_packages)) self.assertEqual(all_packages[0]["type"], "bits") - for droplet in all_packages: - self.assertIsInstance(droplet, Entity) + for package in all_packages: + self.assertIsInstance(package, Entity) def test_get(self): self.client.get.return_value = self.mock_response(