Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,23 @@ Another example:
for task in client.v3.tasks.list(app_guids=['app-guid-1', 'app-guid-2']):
task.cancel()

When supported by the API, parent entities can be included in a single call. The included entities replace the links mentioned above.
The following code snippet issues three requests to the API in order to get app, space and organization data:

.. code-block:: python

app = client.v3.apps.get("app-guid")
print("App name: %s" % app["name])
space = app.space()
print("Space name: %s" % space["name])
org = space.organization()
print("Org name: %s" % org["name])

By changing the first line only, a single request fetches all the data. The navigation from app to space and space to organization remains unchanged.

.. code-block:: python

app = client.v3.apps.get("app-guid", include="space.organization")

Available managers on API V3 are:

Expand All @@ -227,7 +244,10 @@ Available managers on API V3 are:
- ``spaces``
- ``tasks``

The managers provide the same methods as the V2 managers.
The managers provide the same methods as the V2 managers with the following differences:

- ``get(**kwargs)``: supports keyword arguments that are passed on to the API, e.g. "include"


Networking
----------
Expand Down
17 changes: 6 additions & 11 deletions main/cloudfoundry_client/v3/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import functools
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional

from cloudfoundry_client.json_object import JsonObject
from cloudfoundry_client.v3.entities import EntityManager, Entity
Expand All @@ -9,15 +8,11 @@


class App(Entity):
def __init__(self, target_endpoint: str, client: "CloudFoundryClient", **kwargs):
super(App, self).__init__(target_endpoint, client, **kwargs)
# patch environment_variables method
environment_variables_link = self.get("links", {}).get("environment_variables", {}).get("href", None)
if environment_variables_link is not None:
other_manager = self._default_manager(client, target_endpoint)
new_method = functools.partial(other_manager._get, environment_variables_link)
new_method.__name__ = "environment_variables"
setattr(self, "environment_variables", new_method)
@staticmethod
def _manager_method(link_name: str, link_method: str) -> Optional[str]:
if link_name == "environment_variables" and link_method == "get":
return "_get" # instead of _paginate
return Entity._manager_method(link_name, link_method)


class AppManager(EntityManager):
Expand Down
111 changes: 82 additions & 29 deletions main/cloudfoundry_client/v3/entities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import functools
from json import JSONDecodeError
from typing import Any, Generator, Optional, List, Tuple, Union, TypeVar, TYPE_CHECKING
from typing import Any, Generator, Optional, List, Tuple, Union, TypeVar, TYPE_CHECKING, Callable, Type
from urllib.parse import quote, urlparse

from requests import Response
Expand All @@ -10,47 +10,74 @@
from cloudfoundry_client.request_object import Request

if TYPE_CHECKING:
from cloudfoundry_client.client import CloudFoundryClient
from cloudfoundry_client.client import CloudFoundryClient, V3


def plural(name: str) -> str:
return name if name.endswith("s") else "%ss" % name


class Entity(JsonObject):
def __init__(self, target_endpoint: str, client: "CloudFoundryClient", **kwargs):
super(Entity, self).__init__(**kwargs)
try:
default_manager = self._default_manager(target_endpoint, client)
self._create_navigable_links(client.v3, default_manager)
self._create_navigable_included_entities(client.v3, default_manager)

def default_method(m, u):
raise NotImplementedError("Unknown method %s for url %s" % (m, u))

default_manager = self._default_manager(client, target_endpoint)
def _create_navigable_links(self, v3_client: "V3", default_manager: "EntityManager") -> None:
try:
default_method = self._default_method()
for link_name, link in self.get("links", {}).items():
if link_name != "self":
link_method = link.get("method", "GET").lower()
manager_method = self._manager_method(link_name, link_method)
ref = link["href"]
manager_name = link_name if link_name.endswith("s") else "%ss" % link_name
other_manager = getattr(client.v3, manager_name, default_manager)
if link_method == "get":
new_method = (
functools.partial(other_manager._paginate, ref)
if link_name.endswith("s")
else functools.partial(other_manager._get, ref)
)
elif link_method == "post":
new_method = functools.partial(other_manager._post, ref)
elif link_method == "put":
new_method = functools.partial(other_manager._put, ref)
elif link_method == "delete":
new_method = functools.partial(other_manager._delete, ref)
if manager_method is not None:
manager_name = plural(link_name)
other_manager = getattr(v3_client, manager_name, default_manager)
new_method = functools.partial(getattr(other_manager, manager_method), ref)
else:
new_method = functools.partial(default_method, link_method, ref)
new_method.__name__ = link_name
setattr(self, link_name, new_method)
except KeyError:
raise InvalidEntity(**self)

def _create_navigable_included_entities(self, v3_client: "V3", default_manager: "EntityManager") -> None:
for entity_name, entity_data in self.get("_included", {}).items():
manager_name = plural(entity_name)
other_manager = getattr(v3_client, manager_name, default_manager)
entity_type = other_manager._get_entity_type(entity_name)
entity = entity_type(other_manager.target_endpoint, other_manager.client, **entity_data)
setattr(self, entity_name, lambda: entity)
self.pop("_included", None)

@staticmethod
def _default_manager(client, target_endpoint):
def _default_manager(target_endpoint: str, client: "CloudFoundryClient") -> "EntityManager":
return EntityManager(target_endpoint, client, "")

@staticmethod
def _default_method() -> Callable:
def default_method(m, u):
raise NotImplementedError("Unknown method %s for url %s" % (m, u))

return default_method

@staticmethod
def _manager_method(link_name: str, link_method: str) -> Optional[str]:
if link_method == "get":
if link_name.endswith("s"):
return "_paginate"
else:
return "_get"
elif link_method == "post":
return "_post"
elif link_method == "put":
return "_put"
elif link_method == "delete":
return "_delete"
return None


PaginateEntities = Generator[Entity, None, None]

Expand Down Expand Up @@ -101,8 +128,9 @@ def _post(self, url: str, data: Optional[dict] = None, files: Any = None, entity
response = self.client.post(url, json=data, files=files)
return self._read_response(response, entity_type)

def _get(self, url: str, entity_type: Optional[ENTITY_TYPE] = None) -> Entity:
response = self.client.get(url)
def _get(self, url: str, entity_type: Optional[ENTITY_TYPE] = None, **kwargs) -> Entity:
url_requested = EntityManager._get_url_with_encoded_params(url, **kwargs)
response = self.client.get(url_requested)
return self._read_response(response, entity_type)

def _put(self, url: str, data: dict, entity_type: Optional[ENTITY_TYPE] = None) -> Entity:
Expand All @@ -121,7 +149,8 @@ def _delete(self, url: str) -> Optional[str]:
return None

def _list(self, requested_path: str, entity_type: Optional[ENTITY_TYPE] = None, **kwargs) -> PaginateEntities:
url_requested = EntityManager._get_url_filtered("%s%s" % (self.target_endpoint, requested_path), **kwargs)
url_requested = EntityManager._get_url_with_encoded_params("%s%s" % (self.target_endpoint, requested_path),
**kwargs)
for element in self._paginate(url_requested, entity_type):
yield element

Expand Down Expand Up @@ -177,12 +206,12 @@ def get_first(self, **kwargs) -> Optional[Entity]:
return entity
return None

def get(self, entity_id: str, *extra_paths) -> Entity:
def get(self, entity_id: str, *extra_paths, **kwargs) -> Entity:
if len(extra_paths) == 0:
requested_path = "%s%s/%s" % (self.target_endpoint, self.entity_uri, entity_id)
else:
requested_path = "%s%s/%s/%s" % (self.target_endpoint, self.entity_uri, entity_id, "/".join(extra_paths))
return self._get(requested_path)
return self._get(requested_path, **kwargs)

def _read_response(self, response: Response, entity_type: Optional[ENTITY_TYPE]) -> Union[JsonObject, Entity]:
try:
Expand All @@ -197,20 +226,44 @@ def _read_response(self, response: Response, entity_type: Optional[ENTITY_TYPE])
"method": "GET",
}

return self._entity(result, entity_type)
return self._entity(self._mixin_included_resources(result), entity_type)

@staticmethod
def _request(**mandatory_parameters) -> Request:
return Request(**mandatory_parameters)

def _mixin_included_resources(self, result: JsonObject) -> JsonObject:
if "included" not in result:
return result
for resource in result.get("resources", [result]):
self._include_resources(resource, result)
del result["included"]
return result

def _include_resources(self, resource: JsonObject, result: JsonObject) -> None:
for relationship_name, relationship in resource.get("relationships", {}).items():
relationship_guid = relationship.get("data", {}).get("guid", None)
included_resources = result["included"].get(plural(relationship_name), None)
if relationship_guid is not None and included_resources is not None:
included_resource = next((r for r in included_resources if relationship_guid == r.get("guid", None)),
None)
if included_resource is not None:
self._include_resources(included_resource, result)
included = resource.setdefault("_included", {})
included.update({relationship_name: included_resource})

@staticmethod
def _get_entity_type(entity_name: str) -> Type[ENTITY_TYPE]:
return Entity

def _entity(self, result: JsonObject, entity_type: Optional[ENTITY_TYPE]) -> Union[JsonObject, Entity]:
if "guid" in result or ("links" in result and "job" in result["links"]):
return (entity_type or self.entity_type)(self.target_endpoint, self.client, **result)
else:
return result

@staticmethod
def _get_url_filtered(url: str, **kwargs) -> str:
def _get_url_with_encoded_params(url: str, **kwargs) -> str:
def _append_encoded_parameter(parameters: List[str], args: Tuple[str, Any]) -> List[str]:
parameter_name, parameter_value = args[0], args[1]
if isinstance(parameter_value, (list, tuple)):
Expand Down
Loading