From f3ca15d569b253b2b562b8e06689e47ee92d8a8d Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:38:43 +0200 Subject: [PATCH 1/8] fix(networking): Update entities _get_url_filtered function fix(networking): Update _append_encoded_parameter function to append the parameters with the %s=%s format, in order to be compliant with the Policy Server API interface --- cloudfoundry_client/networking/entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudfoundry_client/networking/entities.py b/cloudfoundry_client/networking/entities.py index da393d6..07d34e7 100644 --- a/cloudfoundry_client/networking/entities.py +++ b/cloudfoundry_client/networking/entities.py @@ -116,9 +116,9 @@ def _append_encoded_parameter(parameters: List[str], args: Tuple[str, Any]) -> L for value in value_list: parameters.append("%s=%s" % (parameter_name, str(value))) elif isinstance(parameter_value, (list, tuple)): - parameters.append("q=%s" % quote("%s IN %s" % (parameter_name, ",".join(parameter_value)))) + parameters.append("%s=%s" % (parameter_name, quote(",".join(parameter_value)))) else: - parameters.append("q=%s" % quote("%s:%s" % (parameter_name, str(parameter_value)))) + parameters.append("%s=%s" % (parameter_name, quote(str(parameter_value)))) return parameters if len(kwargs) > 0: From 6f289ee10385c2e846c1bb3800be1a2a930bd769 Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:47:21 +0200 Subject: [PATCH 2/8] feat(apps): Add function for the /v3/apps/:guid/manifest endpoint --- cloudfoundry_client/v3/apps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py index 1453575..4382ea2 100644 --- a/cloudfoundry_client/v3/apps.py +++ b/cloudfoundry_client/v3/apps.py @@ -24,3 +24,6 @@ def get_env(self, application_guid: str) -> JsonObject: def get_routes(self, application_guid: str) -> JsonObject: return super(AppManager, self)._get("%s%s/%s/routes" % (self.target_endpoint, self.entity_uri, application_guid)) + + 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 From ac205012251ccb753e01827c064409156b53180f Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Sat, 5 Jul 2025 19:14:12 +0200 Subject: [PATCH 3/8] test: Add test for the v3 apps get_manifest function --- .../v3/apps/GET_{id}_manifest_response.yml | 28 +++++++++++++++++++ tests/v3/test_apps.py | 28 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/fixtures/v3/apps/GET_{id}_manifest_response.yml diff --git a/tests/fixtures/v3/apps/GET_{id}_manifest_response.yml b/tests/fixtures/v3/apps/GET_{id}_manifest_response.yml new file mode 100644 index 0000000..f263616 --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_manifest_response.yml @@ -0,0 +1,28 @@ +--- +applications: + - name: my-app + stack: cflinuxfs4 + features: + ssh: true + revisions: true + service-binding-k8s: false + file-based-vcap-services: false + services: + - my-service + routes: + - route: my-app.example.com + protocol: http1 + processes: + - type: web + instances: 2 + memory: 512M + log-rate-limit-per-second: 1KB + disk_quota: 1024M + health-check-type: http + health-check-http-endpoint: /healthy + health-check-invocation-timeout: 10 + health-check-interval: 5 + readiness-health-check-type: http + readiness-health-check-http-endpoint: /ready + readiness-health-check-invocation-timeout: 20 + readiness-health-check-interval: 5 \ No newline at end of file diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py index c601170..b8a6432 100644 --- a/tests/v3/test_apps.py +++ b/tests/v3/test_apps.py @@ -1,5 +1,7 @@ import unittest +import yaml from http import HTTPStatus +from typing import Optional, List, Union from abstract_test_case import AbstractTestCase from cloudfoundry_client.common_objects import JsonObject @@ -132,3 +134,29 @@ def test_list_include_space(self): self.assertIsInstance(all_spaces[0], Entity) self.assertEqual(all_spaces[1]["name"], "my_space") self.assertIsInstance(all_spaces[1], Entity) + + def test_get_manifest(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id/manifest", HTTPStatus.OK, {"Content-Type": "application/x-yaml"}, "v3", "apps", + "GET_{id}_manifest_response.yml" + ) + manifest_response: str = self.client.v3.apps.get_manifest("app_id") + self.assertIsInstance(manifest_response, str) + manifest: dict = yaml.safe_load(manifest_response) + applications: Optional[list[dict]] = manifest.get("applications") + self.assertIsInstance(applications, list) + self.assertEqual(len(applications), 1) + application: dict = applications[0] + self.assertEqual(application.get("name"), "my-app") + self.assertEqual(application.get("stack"), "cflinuxfs4") + application_services: Optional[list[str]] = application.get("services") + self.assertIsInstance(application_services, list) + self.assertEqual(len(application_services),1) + self.assertEqual(application_services[0],"my-service") + application_routes: Optional[List[Union[dict,str]]] = application.get("routes") + self.assertIsInstance(application_routes,list) + self.assertEqual(len(application_routes),1) + application_route: dict = application_routes[0] + self.assertIsInstance(application_route,dict) + self.assertEqual(application_route.get("route"),"my-app.example.com") + self.assertEqual(application_route.get("protocol"),"http1") \ No newline at end of file From 206b0f285eba831b09a7fb28624550a39d5f5b7d Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Sat, 5 Jul 2025 20:31:20 +0200 Subject: [PATCH 4/8] test: Fix formatting of statements in test_get_manifest function --- tests/v3/test_apps.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py index b8a6432..8aeb0cf 100644 --- a/tests/v3/test_apps.py +++ b/tests/v3/test_apps.py @@ -151,12 +151,12 @@ def test_get_manifest(self): self.assertEqual(application.get("stack"), "cflinuxfs4") application_services: Optional[list[str]] = application.get("services") self.assertIsInstance(application_services, list) - self.assertEqual(len(application_services),1) - self.assertEqual(application_services[0],"my-service") - application_routes: Optional[List[Union[dict,str]]] = application.get("routes") - self.assertIsInstance(application_routes,list) - self.assertEqual(len(application_routes),1) + self.assertEqual(len(application_services), 1) + self.assertEqual(application_services[0], "my-service") + application_routes: Optional[List[Union[dict, str]]] = application.get("routes") + self.assertIsInstance(application_routes, list) + self.assertEqual(len(application_routes), 1) application_route: dict = application_routes[0] - self.assertIsInstance(application_route,dict) - self.assertEqual(application_route.get("route"),"my-app.example.com") - self.assertEqual(application_route.get("protocol"),"http1") \ No newline at end of file + self.assertIsInstance(application_route, dict) + self.assertEqual(application_route.get("route"), "my-app.example.com") + self.assertEqual(application_route.get("protocol"), "http1") From 0c1ae1e386eeebba21ee9bd6d06794c96e2fca57 Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:08:06 +0200 Subject: [PATCH 5/8] feat(apps): Update calls to _list --- cloudfoundry_client/v3/apps.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py index 4382ea2..fe20545 100644 --- a/cloudfoundry_client/v3/apps.py +++ b/cloudfoundry_client/v3/apps.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, Optional -from cloudfoundry_client.common_objects import JsonObject -from cloudfoundry_client.v3.entities import EntityManager +from cloudfoundry_client.common_objects import JsonObject, Pagination +from cloudfoundry_client.v3.entities import EntityManager, Entity if TYPE_CHECKING: from cloudfoundry_client.client import CloudFoundryClient @@ -27,3 +27,11 @@ def get_routes(self, application_guid: str) -> JsonObject: 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 get_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) + + def get_deployed_revisions(self, application_guid: str,**kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/revisions/deployed" % (self.entity_uri, application_guid) + return super(AppManager,self)._list(requested_path=uri, **kwargs) \ No newline at end of file From a58df2c15847db44de528323bd5076ed6844f55f Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:30:04 +0200 Subject: [PATCH 6/8] test(apps): Add unit tests for revisions and deployed revisions endpoints docs: Update README.rst --- README.rst | 3 +- .../GET_{id}_deployed_revisions_response.json | 62 +++++++++++++++++++ .../v3/apps/GET_{id}_revisions_response.json | 62 +++++++++++++++++++ tests/v3/test_apps.py | 31 +++++++++- 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/v3/apps/GET_{id}_deployed_revisions_response.json create mode 100644 tests/fixtures/v3/apps/GET_{id}_revisions_response.json diff --git a/README.rst b/README.rst index eb52a8e..e8466d6 100644 --- a/README.rst +++ b/README.rst @@ -397,4 +397,5 @@ You can run tests by doing so. In the project directory: $ export PYTHONPATH=main $ python -m unittest discover test # or even - $ python setup.py test + $ poetry install + $ poetry run pytest \ No newline at end of file diff --git a/tests/fixtures/v3/apps/GET_{id}_deployed_revisions_response.json b/tests/fixtures/v3/apps/GET_{id}_deployed_revisions_response.json new file mode 100644 index 0000000..920f448 --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_deployed_revisions_response.json @@ -0,0 +1,62 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920", + "version": 1, + "droplet": { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "processes": { + "web": { + "command": "bundle exec rackup" + } + }, + "sidecars": [ + { + "name": "auth-sidecar", + "command": "bundle exec sidecar", + "process_types": ["web"], + "memory_in_mb": 300 + } + ], + "description": "Initial revision.", + "deployable": true, + "relationships": { + "app": { + "data": { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446" + } + } + }, + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/revisions/885735b5-aea4-4cf5-8e44-961af0e41920" + }, + "app": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446" + }, + "environment_variables": { + "href": "https://api.example.org/v3/revisions/885735b5-aea4-4cf5-8e44-961af0e41920/environment_variables" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/apps/GET_{id}_revisions_response.json b/tests/fixtures/v3/apps/GET_{id}_revisions_response.json new file mode 100644 index 0000000..920f448 --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_revisions_response.json @@ -0,0 +1,62 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920", + "version": 1, + "droplet": { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "processes": { + "web": { + "command": "bundle exec rackup" + } + }, + "sidecars": [ + { + "name": "auth-sidecar", + "command": "bundle exec sidecar", + "process_types": ["web"], + "memory_in_mb": 300 + } + ], + "description": "Initial revision.", + "deployable": true, + "relationships": { + "app": { + "data": { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446" + } + } + }, + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/revisions/885735b5-aea4-4cf5-8e44-961af0e41920" + }, + "app": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446" + }, + "environment_variables": { + "href": "https://api.example.org/v3/revisions/885735b5-aea4-4cf5-8e44-961af0e41920/environment_variables" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py index 8aeb0cf..0647c31 100644 --- a/tests/v3/test_apps.py +++ b/tests/v3/test_apps.py @@ -4,7 +4,7 @@ from typing import Optional, List, Union from abstract_test_case import AbstractTestCase -from cloudfoundry_client.common_objects import JsonObject +from cloudfoundry_client.common_objects import JsonObject, Pagination from cloudfoundry_client.v3.entities import Entity @@ -160,3 +160,32 @@ def test_get_manifest(self): self.assertIsInstance(application_route, dict) self.assertEqual(application_route.get("route"), "my-app.example.com") self.assertEqual(application_route.get("protocol"), "http1") + + def test_get_revisions(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_guid/revisions", HTTPStatus.OK, {"Content-Type": "application/json"}, "v3", "apps", + "GET_{id}_revisions_response.json" + ) + revisions_response: Pagination[Entity] = self.client.v3.apps.get_revisions("app_guid") + revisions: list[dict] = [revision for revision in revisions_response] + self.assertIsInstance(revisions, list) + self.assertEqual(len(revisions),1) + revision: dict = revisions[0] + self.assertIsInstance(revision,dict) + self.assertEqual(revision.get("guid"),"885735b5-aea4-4cf5-8e44-961af0e41920") + self.assertEqual(revision.get("description"),"Initial revision.") + self.assertEqual(revision.get("deployable"),True) + + def test_get_deployed_revisions(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_guid/revisions/deployed", HTTPStatus.OK, {"Content-Type": "application/json"}, "v3", "apps", + "GET_{id}_deployed_revisions_response.json" + ) + revisions_response: Pagination[Entity] = self.client.v3.apps.get_deployed_revisions("app_guid") + revisions: list[dict] = [revision for revision in revisions_response] + self.assertIsInstance(revisions, list) + self.assertEqual(len(revisions),1) + revision: dict = revisions[0] + self.assertIsInstance(revision,dict) + self.assertEqual(revision.get("created_at"),"2017-02-01T01:33:58Z") + self.assertEqual(revision.get("version"),1) \ No newline at end of file From 93b9e722863ae906406b66c6a045d249de5e4ca2 Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:37:54 +0200 Subject: [PATCH 7/8] chore: Fix linting issues --- cloudfoundry_client/v3/apps.py | 8 ++++---- tests/v3/test_apps.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py index fe20545..32af487 100644 --- a/cloudfoundry_client/v3/apps.py +++ b/cloudfoundry_client/v3/apps.py @@ -28,10 +28,10 @@ def get_routes(self, application_guid: str) -> JsonObject: 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 get_revisions(self, application_guid: str,**kwargs) -> Pagination[Entity]: + def get_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) + return super(AppManager, self)._list(requested_path=uri, **kwargs) - def get_deployed_revisions(self, application_guid: str,**kwargs) -> Pagination[Entity]: + def get_deployed_revisions(self, application_guid: str, **kwargs) -> Pagination[Entity]: uri: str = "%s/%s/revisions/deployed" % (self.entity_uri, application_guid) - return super(AppManager,self)._list(requested_path=uri, **kwargs) \ No newline at end of file + return super(AppManager, self)._list(requested_path=uri, **kwargs) diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py index 0647c31..eaa1eef 100644 --- a/tests/v3/test_apps.py +++ b/tests/v3/test_apps.py @@ -169,12 +169,12 @@ def test_get_revisions(self): revisions_response: Pagination[Entity] = self.client.v3.apps.get_revisions("app_guid") revisions: list[dict] = [revision for revision in revisions_response] self.assertIsInstance(revisions, list) - self.assertEqual(len(revisions),1) + self.assertEqual(len(revisions), 1) revision: dict = revisions[0] - self.assertIsInstance(revision,dict) - self.assertEqual(revision.get("guid"),"885735b5-aea4-4cf5-8e44-961af0e41920") - self.assertEqual(revision.get("description"),"Initial revision.") - self.assertEqual(revision.get("deployable"),True) + self.assertIsInstance(revision, dict) + self.assertEqual(revision.get("guid"), "885735b5-aea4-4cf5-8e44-961af0e41920") + self.assertEqual(revision.get("description"), "Initial revision.") + self.assertEqual(revision.get("deployable"), True) def test_get_deployed_revisions(self): self.client.get.return_value = self.mock_response( @@ -184,8 +184,8 @@ def test_get_deployed_revisions(self): revisions_response: Pagination[Entity] = self.client.v3.apps.get_deployed_revisions("app_guid") revisions: list[dict] = [revision for revision in revisions_response] self.assertIsInstance(revisions, list) - self.assertEqual(len(revisions),1) + self.assertEqual(len(revisions), 1) revision: dict = revisions[0] - self.assertIsInstance(revision,dict) - self.assertEqual(revision.get("created_at"),"2017-02-01T01:33:58Z") - self.assertEqual(revision.get("version"),1) \ No newline at end of file + self.assertIsInstance(revision, dict) + self.assertEqual(revision.get("created_at"), "2017-02-01T01:33:58Z") + self.assertEqual(revision.get("version"), 1) From 12f29f5af5514ea1401d2022800321f8641d666f Mon Sep 17 00:00:00 2001 From: David Duarte <102606398+daviddmd@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:41:28 +0200 Subject: [PATCH 8/8] fix: Rename from get to list revisions --- cloudfoundry_client/v3/apps.py | 4 ++-- tests/v3/test_apps.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py index 32af487..5ec8486 100644 --- a/cloudfoundry_client/v3/apps.py +++ b/cloudfoundry_client/v3/apps.py @@ -28,10 +28,10 @@ def get_routes(self, application_guid: str) -> JsonObject: 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 get_revisions(self, application_guid: str, **kwargs) -> Pagination[Entity]: + 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) - def get_deployed_revisions(self, application_guid: str, **kwargs) -> Pagination[Entity]: + def list_deployed_revisions(self, application_guid: str, **kwargs) -> Pagination[Entity]: uri: str = "%s/%s/revisions/deployed" % (self.entity_uri, application_guid) return super(AppManager, self)._list(requested_path=uri, **kwargs) diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py index eaa1eef..5c9ed9d 100644 --- a/tests/v3/test_apps.py +++ b/tests/v3/test_apps.py @@ -161,12 +161,12 @@ def test_get_manifest(self): self.assertEqual(application_route.get("route"), "my-app.example.com") self.assertEqual(application_route.get("protocol"), "http1") - def test_get_revisions(self): + def test_list_revisions(self): self.client.get.return_value = self.mock_response( "/v3/apps/app_guid/revisions", HTTPStatus.OK, {"Content-Type": "application/json"}, "v3", "apps", "GET_{id}_revisions_response.json" ) - revisions_response: Pagination[Entity] = self.client.v3.apps.get_revisions("app_guid") + revisions_response: Pagination[Entity] = self.client.v3.apps.list_revisions("app_guid") revisions: list[dict] = [revision for revision in revisions_response] self.assertIsInstance(revisions, list) self.assertEqual(len(revisions), 1) @@ -176,12 +176,12 @@ def test_get_revisions(self): self.assertEqual(revision.get("description"), "Initial revision.") self.assertEqual(revision.get("deployable"), True) - def test_get_deployed_revisions(self): + def test_list_deployed_revisions(self): self.client.get.return_value = self.mock_response( "/v3/apps/app_guid/revisions/deployed", HTTPStatus.OK, {"Content-Type": "application/json"}, "v3", "apps", "GET_{id}_deployed_revisions_response.json" ) - revisions_response: Pagination[Entity] = self.client.v3.apps.get_deployed_revisions("app_guid") + revisions_response: Pagination[Entity] = self.client.v3.apps.list_deployed_revisions("app_guid") revisions: list[dict] = [revision for revision in revisions_response] self.assertIsInstance(revisions, list) self.assertEqual(len(revisions), 1)