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`` diff --git a/cloudfoundry_client/v3/entities.py b/cloudfoundry_client/v3/entities.py index f793f84..7fa7d23 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, ",".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..f461e5b 100644 --- a/tests/v3/test_service_instances.py +++ b/tests/v3/test_service_instances.py @@ -99,6 +99,57 @@ 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")