From 7444549649fa6e672ebf8238479d80f50895a8c1 Mon Sep 17 00:00:00 2001 From: Krzysztof <38418755+krismarc@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:39:51 +0200 Subject: [PATCH 1/4] Update entities.py Fields support. https://v3-apidocs.cloudfoundry.org/version/3.197.0/index.html#fields As specified in the API description. Navigable object would contain only data from includes returned by fields query params. --- cloudfoundry_client/v3/entities.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloudfoundry_client/v3/entities.py b/cloudfoundry_client/v3/entities.py index f793f84..45775b3 100644 --- a/cloudfoundry_client/v3/entities.py +++ b/cloudfoundry_client/v3/entities.py @@ -300,6 +300,9 @@ def _append_encoded_parameter(parameters: List[str], args: Tuple[str, Any]) -> L parameter_name, parameter_value = args[0], args[1] if isinstance(parameter_value, (list, tuple)): parameters.append("%s=%s" % (parameter_name, quote(",".join(parameter_value)))) + elif isinstance(parameter_value, (dict)) and parameter_name == "fields": + for resource, key in parameter_value.items(): + parameters.append("%s[%s]=%s" % (parameter_name, resource, quote(",".join(key)))) else: parameters.append("%s=%s" % (parameter_name, quote(str(parameter_value)))) return parameters From 6472c32cdd4b3300353ec1d2e74d1225f9b06dd1 Mon Sep 17 00:00:00 2001 From: Krzysztof Marciniak Date: Wed, 30 Jul 2025 12:46:04 +0200 Subject: [PATCH 2/4] tests + removed additional quotation in entities.py --- cloudfoundry_client/v3/entities.py | 2 +- .../GET_response_fields_space_and_org.json | 139 ++++++++++++++++++ .../GET_{id}_response_fields_space.json | 71 +++++++++ tests/v3/test_service_instances.py | 47 ++++++ 4 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/v3/service_instances/GET_response_fields_space_and_org.json create mode 100644 tests/fixtures/v3/service_instances/GET_{id}_response_fields_space.json diff --git a/cloudfoundry_client/v3/entities.py b/cloudfoundry_client/v3/entities.py index 45775b3..7fa7d23 100644 --- a/cloudfoundry_client/v3/entities.py +++ b/cloudfoundry_client/v3/entities.py @@ -302,7 +302,7 @@ def _append_encoded_parameter(parameters: List[str], args: Tuple[str, Any]) -> L parameters.append("%s=%s" % (parameter_name, quote(",".join(parameter_value)))) elif isinstance(parameter_value, (dict)) and parameter_name == "fields": for resource, key in parameter_value.items(): - parameters.append("%s[%s]=%s" % (parameter_name, resource, quote(",".join(key)))) + parameters.append("%s[%s]=%s" % (parameter_name, resource, ",".join(key))) else: parameters.append("%s=%s" % (parameter_name, quote(str(parameter_value)))) return parameters diff --git a/tests/fixtures/v3/service_instances/GET_response_fields_space_and_org.json b/tests/fixtures/v3/service_instances/GET_response_fields_space_and_org.json new file mode 100644 index 0000000..6f0830e --- /dev/null +++ b/tests/fixtures/v3/service_instances/GET_response_fields_space_and_org.json @@ -0,0 +1,139 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://somewhere.com/v3/service_instances?fields%5Bspace%5D=guid%2Cname%2Crelationships.organization&fields%5Bspace.organization%5D=guid%2Cname&page=1&per_page=50" + }, + "last": { + "href": "https://somewhere.com/v3/service_instances?fields%5Bspace%5D=guid%2Cname%2Crelationships.organization&fields%5Bspace.organization%5D=guid%2Cname&page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "147119a3-53e7-41af-8d06-46806695ae1a", + "created_at": "2025-07-30T07:55:53Z", + "updated_at": "2025-07-30T07:55:53Z", + "name": "my-user-provided-service", + "tags": [], + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "Operation succeeded", + "updated_at": "2025-07-30T07:55:53Z", + "created_at": "2025-07-30T07:55:53Z" + }, + "type": "user-provided", + "syslog_drain_url": null, + "route_service_url": null, + "relationships": { + "space": { + "data": { + "guid": "aa3c5cfd-3f75-43f3-aac8-216fec6b3be5" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://somewhere.com/v3/service_instances/147119a3-53e7-41af-8d06-46806695ae1a" + }, + "space": { + "href": "https://somewhere.com/v3/spaces/aa3c5cfd-3f75-43f3-aac8-216fec6b3be5" + }, + "service_credential_bindings": { + "href": "https://somewhere.com/v3/service_credential_bindings?service_instance_guids=147119a3-53e7-41af-8d06-46806695ae1a" + }, + "service_route_bindings": { + "href": "https://somewhere.com/v3/service_route_bindings?service_instance_guids=147119a3-53e7-41af-8d06-46806695ae1a" + }, + "credentials": { + "href": "https://somewhere.com/v3/service_instances/147119a3-53e7-41af-8d06-46806695ae1a/credentials" + } + } + }, + { + "guid": "858e2101-ebb3-4c62-af6d-06e26bae744c", + "created_at": "2025-07-30T07:57:04Z", + "updated_at": "2025-07-30T07:57:05Z", + "name": "my-managed-service", + "tags": [], + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "", + "updated_at": "2025-07-30T07:57:05Z", + "created_at": "2025-07-30T07:57:05Z" + }, + "type": "managed", + "maintenance_info": {}, + "upgrade_available": false, + "dashboard_url": null, + "relationships": { + "space": { + "data": { + "guid": "aa3c5cfd-3f75-43f3-aac8-216fec6b3be5" + } + }, + "service_plan": { + "data": { + "guid": "a73e54c2-cc12-4fc5-8f8d-4eec3e6c383c" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://somewhere.com/v3/service_instances/858e2101-ebb3-4c62-af6d-06e26bae744c" + }, + "space": { + "href": "https://somewhere.com/v3/spaces/aa3c5cfd-3f75-43f3-aac8-216fec6b3be5" + }, + "service_credential_bindings": { + "href": "https://somewhere.com/v3/service_credential_bindings?service_instance_guids=858e2101-ebb3-4c62-af6d-06e26bae744c" + }, + "service_route_bindings": { + "href": "https://somewhere.com/v3/service_route_bindings?service_instance_guids=858e2101-ebb3-4c62-af6d-06e26bae744c" + }, + "service_plan": { + "href": "https://somewhere.com/v3/service_plans/a73e54c2-cc12-4fc5-8f8d-4eec3e6c383c" + }, + "parameters": { + "href": "https://somewhere.com/v3/service_instances/858e2101-ebb3-4c62-af6d-06e26bae744c/parameters" + }, + "shared_spaces": { + "href": "https://somewhere.com/v3/service_instances/858e2101-ebb3-4c62-af6d-06e26bae744c/relationships/shared_spaces" + } + } + } + ], + "included": { + "spaces": [ + { + "guid": "aa3c5cfd-3f75-43f3-aac8-216fec6b3be5", + "name": "my_space", + "relationships": { + "organization": { + "data": { + "guid": "24ae9e5a-3f0c-4347-8d82-610877534c74" + } + } + } + } + ], + "organizations": [ + { + "guid": "24ae9e5a-3f0c-4347-8d82-610877534c74", + "name": "my_organization" + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_instances/GET_{id}_response_fields_space.json b/tests/fixtures/v3/service_instances/GET_{id}_response_fields_space.json new file mode 100644 index 0000000..edc3211 --- /dev/null +++ b/tests/fixtures/v3/service_instances/GET_{id}_response_fields_space.json @@ -0,0 +1,71 @@ +{ + "guid": "858e2101-ebb3-4c62-af6d-06e26bae744c", + "created_at": "2025-07-30T07:57:04Z", + "updated_at": "2025-07-30T07:57:05Z", + "name": "my-managed-service", + "tags": [], + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "", + "updated_at": "2025-07-30T07:57:05Z", + "created_at": "2025-07-30T07:57:05Z" + }, + "type": "managed", + "maintenance_info": {}, + "upgrade_available": false, + "dashboard_url": null, + "relationships": { + "space": { + "data": { + "guid": "aa3c5cfd-3f75-43f3-aac8-216fec6b3be5" + } + }, + "service_plan": { + "data": { + "guid": "a73e54c2-cc12-4fc5-8f8d-4eec3e6c383c" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://somewhere.com/v3/service_instances/858e2101-ebb3-4c62-af6d-06e26bae744c" + }, + "space": { + "href": "https://somewhere.com/v3/spaces/aa3c5cfd-3f75-43f3-aac8-216fec6b3be5" + }, + "service_credential_bindings": { + "href": "https://somewhere.com/v3/service_credential_bindings?service_instance_guids=858e2101-ebb3-4c62-af6d-06e26bae744c" + }, + "service_route_bindings": { + "href": "https://somewhere.com/v3/service_route_bindings?service_instance_guids=858e2101-ebb3-4c62-af6d-06e26bae744c" + }, + "service_plan": { + "href": "https://somewhere.com/v3/service_plans/a73e54c2-cc12-4fc5-8f8d-4eec3e6c383c" + }, + "parameters": { + "href": "https://somewhere.com/v3/service_instances/858e2101-ebb3-4c62-af6d-06e26bae744c/parameters" + }, + "shared_spaces": { + "href": "https://somewhere.com/v3/service_instances/858e2101-ebb3-4c62-af6d-06e26bae744c/relationships/shared_spaces" + } + }, + "included": { + "spaces": [ + { + "guid": "aa3c5cfd-3f75-43f3-aac8-216fec6b3be5", + "name": "my_space" + } + ], + "organizations": [ + { + "guid": "24ae9e5a-3f0c-4347-8d82-610877534c74", + "name": "my_organization" + } + ] + } +} \ No newline at end of file diff --git a/tests/v3/test_service_instances.py b/tests/v3/test_service_instances.py index 4a08a0c..c3e8b03 100644 --- a/tests/v3/test_service_instances.py +++ b/tests/v3/test_service_instances.py @@ -99,6 +99,53 @@ def test_get(self): self.assertEqual("service_instance_id", service_instance["guid"]) self.assertIsInstance(service_instance, Entity) + def test_get_fields_space(self): + self.client.get.return_value = self.mock_response( + "/v3/service_instances/service_instance_id?fields[space]=guid,name,relationships.organization&fields[space.organization]=guid,name", + HTTPStatus.OK, + None, + "v3", + "service_instances", + "GET_{id}_response_fields_space.json" + ) + fields = { + "space": ["guid,name,relationships.organization"], + "space.organization": ["guid", "name"], + } + space = self.client.v3.service_instances.get("service_instance_id", fields=fields).space() + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my_space", space["name"]) + self.assertIsInstance(space, Entity) + + def test_list_fields_space_and_org(self): + self.client.get.return_value = self.mock_response( + "/v3/service_instances?fields[space]=guid,name,relationships.organization&fields[space.organization]=guid,name", + HTTPStatus.OK, + None, + "v3", + "service_instances", + "GET_response_fields_space_and_org.json" + ) + fields = { + "space": ["guid,name,relationships.organization"], + "space.organization": ["guid","name"] + } + all_spaces = [app.space() for app in self.client.v3.service_instances.list(fields=fields)] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_spaces)) + space1 = all_spaces[0] + self.assertEqual(space1["name"], "my_space") + space1_org = space1.organization() + self.assertEqual(space1_org["name"], "my_organization") + self.assertIsInstance(space1, Entity) + self.assertIsInstance(space1_org, Entity) + space2 = all_spaces[1] + self.assertEqual(space2["name"], "my_space") + space2_org = space2.organization() + self.assertEqual(space2_org["name"], "my_organization") + self.assertIsInstance(space2, Entity) + self.assertIsInstance(space2_org, Entity) + def test_get_then_credentials(self): get_service_instance = self.mock_response( "/v3/service_instances/service_instance_id", HTTPStatus.OK, None, "v3", "service_instances", "GET_{id}_response.json") From d2cb04545cf12eb8b1f227785495e7139753cbed Mon Sep 17 00:00:00 2001 From: Krzysztof Marciniak Date: Wed, 30 Jul 2025 13:02:17 +0200 Subject: [PATCH 3/4] readme update --- README.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.rst b/README.rst index 164cee2..eb52a8e 100644 --- a/README.rst +++ b/README.rst @@ -252,6 +252,16 @@ By changing the first line only, a single request fetches all the data. The navi app = client.v3.apps.get("app-guid", include="space.organization") +.. code-block:: python + + fields = { + "space": ["guid,name,relationships.organization"], + "space.organization": ["guid","name"] + } + services_instances = client.v3.service_instances.list(fields=fields) + +Relationship object generated by `fields` will contain only attributes returned by the API (eg. name, guid). Please note relationship needs to be explicitly requested, otherwise it will be ignored and child object not created. + Available managers on API V3 are: - ``apps`` From 2807464564dc434de5494b4220c9356ba05416a9 Mon Sep 17 00:00:00 2001 From: "Krzysztof Marciniak (external)" Date: Mon, 4 Aug 2025 09:08:37 +0200 Subject: [PATCH 4/4] poetry corrections --- tests/v3/test_service_instances.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/v3/test_service_instances.py b/tests/v3/test_service_instances.py index c3e8b03..f461e5b 100644 --- a/tests/v3/test_service_instances.py +++ b/tests/v3/test_service_instances.py @@ -101,7 +101,9 @@ def test_get(self): def test_get_fields_space(self): self.client.get.return_value = self.mock_response( - "/v3/service_instances/service_instance_id?fields[space]=guid,name,relationships.organization&fields[space.organization]=guid,name", + "/v3/service_instances/service_instance_id" + "?fields[space]=guid,name,relationships.organization" + "&fields[space.organization]=guid,name", HTTPStatus.OK, None, "v3", @@ -111,7 +113,7 @@ def test_get_fields_space(self): fields = { "space": ["guid,name,relationships.organization"], "space.organization": ["guid", "name"], - } + } space = self.client.v3.service_instances.get("service_instance_id", fields=fields).space() self.client.get.assert_called_with(self.client.get.return_value.url) self.assertEqual("my_space", space["name"]) @@ -119,7 +121,9 @@ def test_get_fields_space(self): def test_list_fields_space_and_org(self): self.client.get.return_value = self.mock_response( - "/v3/service_instances?fields[space]=guid,name,relationships.organization&fields[space.organization]=guid,name", + "/v3/service_instances" + "?fields[space]=guid,name,relationships.organization" + "&fields[space.organization]=guid,name", HTTPStatus.OK, None, "v3", @@ -128,8 +132,8 @@ def test_list_fields_space_and_org(self): ) fields = { "space": ["guid,name,relationships.organization"], - "space.organization": ["guid","name"] - } + "space.organization": ["guid", "name"] + } all_spaces = [app.space() for app in self.client.v3.service_instances.list(fields=fields)] self.client.get.assert_called_with(self.client.get.return_value.url) self.assertEqual(2, len(all_spaces))