diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..358025e --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +ignore = E203, W503 +max-line-length = 130 +max-complexity = 16 +buildins = CloudFoundryClient +exclude = vendors,.git,.github,cloudfoundry_client/dropsonde,.venv,.eggs,build + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..806c04f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000..1582906 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,41 @@ +name: Python package + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install and configure Poetry + uses: snok/install-poetry@v1 + with: + virtual envs-create: true + virtualenvs-in-project: true + virtualenvs-path: .venv + installer-parallel: true + + - name: Install project + run: poetry install --no-interaction + + - name: Linting + run: poetry run flake8 --show-source --statistics + + - name: Tests + run: poetry run pytest diff --git a/README.rst b/README.rst index 81d4cdd..e8466d6 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,19 @@ The cf-python-client repo contains a Python client library for Cloud Foundry. Installing ---------- +Supported versions +~~~~~~~~~~~~~~~~~~ + +- Starting version ``1.11.0``, versions older than python ``3.6.0`` will not be supported anymore. This late version was released by the end 2016. + For those that are still using python 2.7, it won't be supported by the end of 2020 and all library shall stop supporting it. +- Starting version ``1.25.0``, versions older than python ``3.7.0`` will not be supported anymore. +- Starting version ``1.36.0``, versions older than python ``3.8.0`` will not be supported anymore. + +See `official documentation`_. + +.. _`official documentation`: https://endoflife.date/python + + From pip ~~~~~~~~ @@ -25,7 +38,7 @@ To build the library run : .. code-block:: bash - $ python setup.py install + $ poetry build Run the client @@ -61,6 +74,40 @@ To instantiate the client, nothing easier client.refresh_token = 'refresh-token' client._access_token = 'access-token' +You can also instantiate the client by reading the config file generated by `cf login`, which allows for authenticating via SSO and LDAP: + +.. code-block:: python + + # init with endpoint & token from the cf cli config file + from cloudfoundry_client.client import CloudFoundryClient + + # use the default file, i.e. ~/.cf/config.json + client = CloudFoundryClient.build_from_cf_config() + # or specify an alternative path + # - other kwargs can be passed through to CloudFoundryClient instantiation + client = CloudFoundryClient.build_from_cf_config(config_path="some/path/config.json", proxy=proxy, verify=False) + +It can also be instantiated with oauth code flow if you possess a dedicated oauth application with its redirection + +.. code-block:: python + + from flask import request + from cloudfoundry_client.client import CloudFoundryClient + target_endpoint = 'https://somewhere.org' + proxy = dict(http=os.environ.get('HTTP_PROXY', ''), https=os.environ.get('HTTPS_PROXY', '')) + client = CloudFoundryClient(target_endpoint, proxy=proxy, verify=False, client_id='my-client-id', client_secret='my-client-secret') + + @app.route('/login') + def login(): + global client + return redirect(client.generate_authorize_url('http://localhost:9999/code', '666')) + + @app.route('/code') + def code(): + global client + client.init_authorize_code_process('http://localhost:9999/code', request.args.get('code')) + + And then you can use it as follows: .. code-block:: python @@ -187,17 +234,87 @@ 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") + +.. 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`` - ``buildpacks`` +- ``domains`` +- ``feature_flags`` +- ``isolation_segments`` +- ``jobs`` - ``organizations`` +- ``organization_quotas`` +- ``processes`` +- ``roles`` +- ``security_groups`` +- ``service_brokers`` +- ``service_credential_bindings`` - ``service_instances`` +- ``service_offerings`` +- ``service_plans`` - ``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 +---------- + +policy server +~~~~~~~~~~~~~ + +At the moment we have only the network policies implemented + +.. code-block:: python + + for policy in client.network.v1.external.policies.list(): + print('destination protocol = {}'.format(policy['destination']['protocol'])) + print('destination from port = {}'.format(policy['destination']['ports']['start'])) + print('destination to port = {}'.format(policy['destination']['ports']['end'])) + + +Available managers on API V3 are: + +- ``policy`` + +This manager provides: + +- ``list(**kwargs)``: return an *iterator* on entities, according to the given filtered parameters +- ``__iter__``: iteration on the manager itself. Alias for a no-filter list +- ``_create``: the create operation. Since it is a generic operation (only takes a *dict* object), this operation is protected +- ``_remove``: the delete operation. This operation is maintained protected. + Application logs ---------------- @@ -224,6 +341,32 @@ Logs can also be streamed using a websocket as follows: # read message infinitely (use break to exit... it will close the underlying websocket) print(log) +.. + +Logs can also be streamed directly from RLP Gateway: + +.. code-block:: python + + import asyncio + from cloudfoundry_client.client import CloudFoundryClient + + target_endpoint = 'https://somewhere.org' + proxy = dict(http=os.environ.get('HTTP_PROXY', ''), https=os.environ.get('HTTPS_PROXY', '')) + rlp_client = CloudFoundryClient(target_endpoint, client_id='client_id', client_secret='client_secret', verify=False) + # init with client credentials + rlp_client.init_with_client_credentials() + + async def get_logs_for_app(rlp_client, app_guid): + async for log in rlp_client.rlpgateway.stream_logs(app_guid, + params={'counter': '', 'gauge': ''}, + headers={'User-Agent': 'cf-python-client'})): + print(log) + + loop = asyncio.get_event_loop() + loop.create_task(get_logs_for_app(rlp_client, "app_guid")) + loop.run_forever() + loop.close() +.. Command Line Interface ---------------------- @@ -254,7 +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/main/cloudfoundry_client/__init__.py b/cloudfoundry_client/__init__.py similarity index 78% rename from main/cloudfoundry_client/__init__.py rename to cloudfoundry_client/__init__.py index 55c3a6c..9100ff8 100644 --- a/main/cloudfoundry_client/__init__.py +++ b/cloudfoundry_client/__init__.py @@ -2,4 +2,4 @@ This module provides a client library for cloudfoundry_client v2/v3. """ -__version__ = "1.10.0" +__version__ = "1.40.3" diff --git a/cloudfoundry_client/client.py b/cloudfoundry_client/client.py new file mode 100644 index 0000000..a4ecfe7 --- /dev/null +++ b/cloudfoundry_client/client.py @@ -0,0 +1,388 @@ +import logging +import os +from pathlib import Path +import json +from http import HTTPStatus + +import requests +from oauth2_client.credentials_manager import CredentialManager, ServiceInformation +from requests import Response + +from cloudfoundry_client.doppler.client import DopplerClient +from cloudfoundry_client.errors import InvalidStatusCode +from cloudfoundry_client.networking.v1.external.policies import PolicyManager +from cloudfoundry_client.rlpgateway.client import RLPGatewayClient +from cloudfoundry_client.v2.apps import AppManager as AppManagerV2 +from cloudfoundry_client.v2.buildpacks import BuildpackManager as BuildpackManagerV2 +from cloudfoundry_client.v2.entities import EntityManager as EntityManagerV2 +from cloudfoundry_client.v2.events import EventManager +from cloudfoundry_client.v2.jobs import JobManager as JobManagerV2 +from cloudfoundry_client.v2.resources import ResourceManager +from cloudfoundry_client.v2.routes import RouteManager as RouteManagerV2 +from cloudfoundry_client.v2.service_bindings import ServiceBindingManager +from cloudfoundry_client.v2.service_brokers import ServiceBrokerManager as ServiceBrokerManagerV2 +from cloudfoundry_client.v2.service_instances import ServiceInstanceManager as ServiceInstanceManagerV2 +from cloudfoundry_client.v2.service_keys import ServiceKeyManager +from cloudfoundry_client.v2.service_plan_visibilities import ServicePlanVisibilityManager +from cloudfoundry_client.v2.service_plans import ServicePlanManager as ServicePlanManagerV2 +from cloudfoundry_client.v2.spaces import SpaceManager as SpaceManagerV2 + +from cloudfoundry_client.v3.apps import AppManager +from cloudfoundry_client.v3.audit_events import AuditEventManager +from cloudfoundry_client.v3.buildpacks import BuildpackManager +from cloudfoundry_client.v3.domains import DomainManager +from cloudfoundry_client.v3.droplets import DropletManager +from cloudfoundry_client.v3.feature_flags import FeatureFlagManager +from cloudfoundry_client.v3.isolation_segments import IsolationSegmentManager +from cloudfoundry_client.v3.organization_quotas import OrganizationQuotaManager +from cloudfoundry_client.v3.packages import PackageManager +from cloudfoundry_client.v3.processes import ProcessManager +from cloudfoundry_client.v3.organizations import OrganizationManager +from cloudfoundry_client.v3.roles import RoleManager +from cloudfoundry_client.v3.routes import RouteManager +from cloudfoundry_client.v3.security_groups import SecurityGroupManager +from cloudfoundry_client.v3.service_brokers import ServiceBrokerManager +from cloudfoundry_client.v3.service_credential_bindings import ServiceCredentialBindingManager +from cloudfoundry_client.v3.service_instances import ServiceInstanceManager +from cloudfoundry_client.v3.service_offerings import ServiceOfferingsManager +from cloudfoundry_client.v3.service_plans import ServicePlanManager +from cloudfoundry_client.v3.spaces import SpaceManager +from cloudfoundry_client.v3.stacks import StackMananager +from cloudfoundry_client.v3.tasks import TaskManager +from cloudfoundry_client.v3.jobs import JobManager +from cloudfoundry_client.v3.users import UserManager + +_logger = logging.getLogger(__name__) + + +class Info: + def __init__( + self, + api_v2_url: str, + api_v3_url: str, + authorization_endpoint: str, + api_endpoint: str, + doppler_endpoint: str | None, + log_stream_endpoint: str | None, + ): + self._api_v2_url = api_v2_url + self._api_v3_url = api_v3_url + self.authorization_endpoint = authorization_endpoint + self.api_endpoint = api_endpoint + self.doppler_endpoint = doppler_endpoint + self.log_stream_endpoint = log_stream_endpoint + + @property + def api_v2_url(self) -> str | None: + return self._api_v2_url + + @property + def api_v3_url(self) -> str | None: + return self._api_v3_url + + +class NetworkingV1External(object): + def __init__(self, target_endpoint: str, credential_manager: "CloudFoundryClient"): + self.policies = PolicyManager(target_endpoint, credential_manager) + + +class V2(object): + def __init__(self, cloud_controller_v2_url: str, credential_manager: "CloudFoundryClient"): + target_endpoint = cloud_controller_v2_url.removesuffix("/v2") + self.apps = AppManagerV2(target_endpoint, credential_manager) + self.buildpacks = BuildpackManagerV2(target_endpoint, credential_manager) + self.jobs = JobManagerV2(target_endpoint, credential_manager) + self.service_bindings = ServiceBindingManager(target_endpoint, credential_manager) + self.service_brokers = ServiceBrokerManagerV2(target_endpoint, credential_manager) + self.service_instances = ServiceInstanceManagerV2(target_endpoint, credential_manager) + self.service_keys = ServiceKeyManager(target_endpoint, credential_manager) + self.service_plan_visibilities = ServicePlanVisibilityManager(target_endpoint, credential_manager) + self.service_plans = ServicePlanManagerV2(target_endpoint, credential_manager) + # Default implementations + self.event = EventManager(target_endpoint, credential_manager) + self.organizations = EntityManagerV2(target_endpoint, credential_manager, "/v2/organizations") + self.private_domains = EntityManagerV2(target_endpoint, credential_manager, "/v2/private_domains") + self.routes = RouteManagerV2(target_endpoint, credential_manager) + self.services = EntityManagerV2(target_endpoint, credential_manager, "/v2/services") + self.shared_domains = EntityManagerV2(target_endpoint, credential_manager, "/v2/shared_domains") + self.spaces = SpaceManagerV2(target_endpoint, credential_manager) + self.stacks = EntityManagerV2(target_endpoint, credential_manager, "/v2/stacks") + self.user_provided_service_instances = EntityManagerV2( + target_endpoint, credential_manager, "/v2/user_provided_service_instances" + ) + self.security_groups = EntityManagerV2(target_endpoint, credential_manager, "/v2/security_groups") + self.users = EntityManagerV2(target_endpoint, credential_manager, "/v2/users") + # Resources implementation used by push operation + self.resources = ResourceManager(target_endpoint, credential_manager) + + +class V3(object): + def __init__(self, cloud_controller_v3_url: str, credential_manager: "CloudFoundryClient"): + target_endpoint = cloud_controller_v3_url.removesuffix("/v3") + self.apps = AppManager(target_endpoint, credential_manager) + self.audit_events = AuditEventManager(target_endpoint, credential_manager) + self.buildpacks = BuildpackManager(target_endpoint, credential_manager) + self.domains = DomainManager(target_endpoint, credential_manager) + self.droplets = DropletManager(target_endpoint, credential_manager) + self.feature_flags = FeatureFlagManager(target_endpoint, credential_manager) + self.isolation_segments = IsolationSegmentManager(target_endpoint, credential_manager) + self.jobs = JobManager(target_endpoint, credential_manager) + self.organizations = OrganizationManager(target_endpoint, credential_manager) + self.organization_quotas = OrganizationQuotaManager(target_endpoint, credential_manager) + self.packages = PackageManager(target_endpoint, credential_manager) + self.processes = ProcessManager(target_endpoint, credential_manager) + self.roles = RoleManager(target_endpoint, credential_manager) + self.routes = RouteManager(target_endpoint, credential_manager) + self.security_groups = SecurityGroupManager(target_endpoint, credential_manager) + self.service_brokers = ServiceBrokerManager(target_endpoint, credential_manager) + self.service_credential_bindings = ServiceCredentialBindingManager(target_endpoint, credential_manager) + self.service_instances = ServiceInstanceManager(target_endpoint, credential_manager) + self.service_offerings = ServiceOfferingsManager(target_endpoint, credential_manager) + self.service_plans = ServicePlanManager(target_endpoint, credential_manager) + self.spaces = SpaceManager(target_endpoint, credential_manager) + self.stacks = StackMananager(target_endpoint, credential_manager) + self.tasks = TaskManager(target_endpoint, credential_manager) + self.users = UserManager(target_endpoint, credential_manager) + + +class CloudFoundryClient(CredentialManager): + def __init__(self, target_endpoint: str, client_id: str = "cf", client_secret: str = "", **kwargs): + """ + :param target_endpoint :the target endpoint. + :param client_id: the client_id + :param client_secret: the client secret + :param proxy: a dict object with entries http and https + :param verify: parameter directly passed to underlying requests library. + (optional) Either a boolean, in which case it controls whether we verify + the server's TLS certificate, or a string, in which case it must be a path + to a CA bundle to use. Defaults to ``True``. + :param token_format: string Can be set to opaque to retrieve an opaque and revocable token. + See UAA API specifications + :param login_hint: string. Indicates the identity provider to be used. + The passed string has to be a URL-Encoded JSON Object, containing the field origin with value as origin_key + of an identity provider. Note that this identity provider must support the grant type password. + See UAA API specifications + :param user_agent: string. Can be used to set a custom http user agent + """ + proxy = kwargs.get("proxy", dict(http="", https="")) + verify = kwargs.get("verify", True) + self.token_format = kwargs.get("token_format") + self.login_hint = kwargs.get("login_hint") + target_endpoint_trimmed = target_endpoint.rstrip("/") + info = self._get_info(target_endpoint_trimmed, proxy, verify=verify) + service_information = ServiceInformation( + None, "%s/oauth/token" % info.authorization_endpoint, client_id, client_secret, [], verify + ) + super().__init__( + service_information, + proxies=proxy, + user_agent=kwargs.get("user_agent", "cf-python-client") + ) + self._v2 = ( + V2(info.api_v2_url, self) + if info.api_v2_url is not None + else None + ) + self._v3 = ( + V3(info.api_v3_url, self) + if info.api_v3_url is not None + else None + ) + self._doppler = ( + DopplerClient( + info.doppler_endpoint, + self.proxies["http" if info.doppler_endpoint.startswith("ws://") else "https"], + self.service_information.verify, + self, + ) + if info.doppler_endpoint is not None + else None + ) + self._rlpgateway = ( + RLPGatewayClient( + info.log_stream_endpoint, + self.proxies["https"], + self.service_information.verify, + self, + ) + if info.log_stream_endpoint is not None + else None + ) + self.networking_v1_external = NetworkingV1External(target_endpoint_trimmed, self) + self.info = info + + @property + def v2(self) -> V2: + if self._v2 is None: + raise NotImplementedError("No V2 endpoint for this instance") + return self._v2 + + @property + def v3(self) -> V3: + if self._v3 is None: + raise NotImplementedError("No V3 endpoint for this instance") + return self._v3 + + @property + def doppler(self) -> DopplerClient: + if self._doppler is None: + raise NotImplementedError("No droppler endpoint for this instance") + else: + + return self._doppler + + @property + def rlpgateway(self): + if self._rlpgateway is None: + raise NotImplementedError("No RLP gateway endpoint for this instance") + else: + return self._rlpgateway + + def _get_info(self, target_endpoint: str, proxy: dict | None = None, verify: bool = True) -> Info: + root_response = CloudFoundryClient._check_response( + requests.get("%s/" % target_endpoint, proxies=proxy if proxy is not None else dict(http="", https=""), verify=verify) + ) + root_info = root_response.json() + + root_links = root_info["links"] + logging = root_links.get("logging") + log_stream = root_links.get("log_stream") + cloud_controller_v2 = root_links.get("cloud_controller_v2") + cloud_controller_v3 = root_links.get("cloud_controller_v3") + return Info( + cloud_controller_v2["href"] if cloud_controller_v2 is not None else None, + cloud_controller_v3["href"] if cloud_controller_v3 is not None else None, + self._resolve_login_endpoint(root_links), + target_endpoint, + logging.get("href") if logging is not None else None, + log_stream.get("href") if log_stream is not None else None, + ) + + @staticmethod + def build_from_cf_config(config_path: str | None = None, **kwargs) -> 'CloudFoundryClient': + cf_home = "CF_HOME" + config_file = "config.json" + config_dir = ".cf" + # handles config path provided or defaults to CF_HOME or HOME + if config_path is not None: + config = Path(config_path) + if config.is_dir(): + if config.name == config_dir: + config = config / config_file + else: + config = config / config_dir / config_file + elif os.environ.get(cf_home): + config = Path(os.environ.get(cf_home)) / config_dir / config_file + else: + config = Path.home() / config_dir / config_file + try: + with open(config) as f: + cf_config = json.load(f) + except Exception as e: + _logger.critical('Could not retrieve cf config: %s', e) + raise + client = CloudFoundryClient(cf_config['Target'], **kwargs) + client.init_with_token(cf_config['RefreshToken']) + return client + + @staticmethod + def _resolve_login_endpoint(root_links): + return (root_links.get("login") or root_links.get("uaa") or root_links.get("self"))["href"] + + @staticmethod + def _is_token_expired(response: Response) -> bool: + if response.status_code == HTTPStatus.UNAUTHORIZED.value: + try: + json_data = response.json() + invalid_token_error = "CF-InvalidAuthToken" + if json_data.get("errors") is not None: # V3 error response + for error in json_data.get("errors"): + if error.get("code", 0) == 1000 and error.get("title", "") == invalid_token_error: + _logger.info("_is_token_v3_expired - true") + return True + _logger.info("_is_token_v3_expired - false") + return False + else: # V2 error response + token_expired = json_data.get("code", 0) == 1000 and json_data.get("error_code", "") == invalid_token_error + _logger.info("_is_token__v2_expired - %s" % str(token_expired)) + return token_expired + except Exception: # noqa: E722 + return False + else: + return False + + @staticmethod + def _token_request_headers(_) -> dict: + return dict(Accept="application/json") + + def __getattr__(self, item): + sub_attr = getattr(self.v2, item, None) + if sub_attr is not None: + return sub_attr + else: + raise AttributeError("type '%s' has no attribute '%s'" % (type(self).__name__, item)) + + def _grant_password_request(self, login: str, password: str) -> dict: + request = super()._grant_password_request(login, password) + if self.token_format is not None: + request["token_format"] = self.token_format + if self.login_hint is not None: + request["login_hint"] = self.login_hint + return request + + def _grant_refresh_token_request(self, refresh_token: str) -> dict: + request = super()._grant_refresh_token_request(refresh_token) + if self.token_format is not None: + request["token_format"] = self.token_format + return request + + def _grant_client_credentials_request(self) -> dict: + return dict( + grant_type="client_credentials", + scope=" ".join(self.service_information.scopes), + client_id=self.service_information.client_id, + client_secret=self.service_information.client_secret, + ) + + def get(self, url: str, params: dict | None = None, **kwargs) -> Response: + response = super().get(url, params, **kwargs) + CloudFoundryClient._log_request("GET", url, response) + return CloudFoundryClient._check_response(response) + + def post(self, url: str, data=None, json=None, **kwargs) -> Response: + response = super().post(url, data, json, **kwargs) + CloudFoundryClient._log_request("POST", url, response) + return CloudFoundryClient._check_response(response) + + def put(self, url: str, data=None, json=None, **kwargs) -> Response: + response = super().put(url, data, json, **kwargs) + CloudFoundryClient._log_request("PUT", url, response) + return CloudFoundryClient._check_response(response) + + def patch(self, url: str, data=None, json=None, **kwargs) -> Response: + response = super().patch(url, data, json, **kwargs) + CloudFoundryClient._log_request("PATCH", url, response) + return CloudFoundryClient._check_response(response) + + def delete(self, url: str, **kwargs) -> Response: + response = super().delete(url, **kwargs) + CloudFoundryClient._log_request("DELETE", url, response) + return CloudFoundryClient._check_response(response) + + @staticmethod + def _log_request(method: str, url: str, response: Response): + _logger.debug( + f"{method}: url={url} - status_code={response.status_code}" + f" - vcap-request-id={response.headers.get('x-vcap-request-id', 'N/A')} - response={response.text}" + ) + + @staticmethod + def _check_response(response: Response) -> Response: + if int(response.status_code / 100) == 2: + return response + else: + try: + body = response.json() + except ValueError: + body = response.text + raise InvalidStatusCode(HTTPStatus(response.status_code), body, response.headers.get("x-vcap-request-id")) diff --git a/cloudfoundry_client/common_objects.py b/cloudfoundry_client/common_objects.py new file mode 100644 index 0000000..4a03083 --- /dev/null +++ b/cloudfoundry_client/common_objects.py @@ -0,0 +1,63 @@ +import json +from collections.abc import Callable, Generator +from typing import TypeVar, Generic + + +class Request(dict): + def __setitem__(self, key, value): + if value is not None: + super().__setitem__(key, value) + + +class JsonObject(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + json = json.dumps + + +ENTITY = TypeVar('ENTITY') + + +class Pagination(Generic[ENTITY], Generator[ENTITY, None, None]): + def __init__(self, first_page: JsonObject, + total_result: int, + next_page_loader: Callable[[JsonObject], JsonObject | None], + resources_accessor: Callable[[JsonObject], list[JsonObject]], + instance_creator: Callable[[JsonObject], ENTITY]): + self._first_page = first_page + self._total_results = total_result + self._next_page_loader = next_page_loader + self._resources_accessor = resources_accessor + self._instance_creator = instance_creator + self._cursor = None + self._current_page = None + + @property + def total_results(self) -> int: + return self._total_results + + def send(self, value) -> ENTITY: + try: + if self._cursor is None: + self._current_page = self._first_page + self._cursor = self._resources_accessor(self._current_page).__iter__() + return self._instance_creator(self._cursor.__next__()) + except StopIteration: + self._current_page = self._next_page_loader(self._current_page) + if self._current_page is None: + raise + self._cursor = self._resources_accessor(self._current_page).__iter__() + return self._instance_creator(self._cursor.__next__()) + + def throw(self, typ, val=None, tb=None): + super().throw(typ, val, tb) + + def close(self): + super().close() + + def __iter__(self): + return self + + def __next__(self) -> ENTITY: + return self.send(None) diff --git a/main/__init__.py b/cloudfoundry_client/doppler/__init__.py similarity index 100% rename from main/__init__.py rename to cloudfoundry_client/doppler/__init__.py diff --git a/main/cloudfoundry_client/doppler/client.py b/cloudfoundry_client/doppler/client.py similarity index 52% rename from main/cloudfoundry_client/doppler/client.py rename to cloudfoundry_client/doppler/client.py index 622985c..4081e59 100644 --- a/main/cloudfoundry_client/doppler/client.py +++ b/cloudfoundry_client/doppler/client.py @@ -1,73 +1,86 @@ import logging import re -from cloudfoundry_client.imported import urlparse +from collections.abc import Generator +from urllib.parse import urlparse + +from oauth2_client.credentials_manager import CredentialManager +from requests import Response -from cloudfoundry_client.imported import bufferize_string from cloudfoundry_client.doppler.websocket_envelope_reader import WebsocketFrameReader from cloudfoundry_client.dropsonde.envelope_pb2 import Envelope from cloudfoundry_client.errors import InvalidLogResponseException _logger = logging.getLogger(__name__) +EnvelopeStream = Generator[Envelope, None, None] + class DopplerClient(object): - def __init__(self, doppler_endpoint, proxy, verify_ssl, credentials_manager): + def __init__(self, doppler_endpoint: str, proxy: str, verify_ssl: bool, credentials_manager: CredentialManager): self.proxy_host = None self.proxy_port = None + self.proxy_auth = None self.ws_doppler_endpoint = doppler_endpoint - self.http_doppler_endpoint = re.sub('^ws', 'http', doppler_endpoint) + self.http_doppler_endpoint = re.sub("^ws", "http", doppler_endpoint) self.verify_ssl = verify_ssl self.credentials_manager = credentials_manager if proxy is not None and len(proxy) > 0: - proxy_domain = urlparse(proxy).netloc - idx = proxy_domain.find(':') - if 0 < idx < len(proxy_domain) - 2: - self.proxy_host = proxy_domain[:idx] - self.proxy_port = int(proxy_domain[idx + 1:]) + proxy_parsed = urlparse(proxy) + self.proxy_host = proxy_parsed.hostname + if proxy_parsed.port is not None: + self.proxy_port = proxy_parsed.port + else: + self.proxy_port = 443 if proxy_parsed.scheme == "https" else 80 + if proxy_parsed.username is not None and proxy_parsed.password is not None: + self.proxy_auth = (proxy_parsed.username, proxy_parsed.password) - def recent_logs(self, app_guid): - url = '%s/apps/%s/recentlogs' % (self.http_doppler_endpoint, app_guid) + def recent_logs(self, app_guid: str) -> EnvelopeStream: + url = "%s/apps/%s/recentlogs" % (self.http_doppler_endpoint, app_guid) response = self.credentials_manager.get(url, stream=True) boundary = DopplerClient._extract_boundary(response) - _logger.debug('Boundary: %s' % boundary) + _logger.debug("Boundary: %s" % boundary) for part in DopplerClient._read_multi_part_response(response, boundary): yield DopplerClient._parse_envelope(part) - def stream_logs(self, app_guid): - url = '%s/apps/%s/stream' % (self.ws_doppler_endpoint, app_guid) - with WebsocketFrameReader(url, - lambda: self.credentials_manager._access_token, - verify_ssl=self.verify_ssl, - proxy_host=self.proxy_host, proxy_port=self.proxy_port) as websocket: + def stream_logs(self, app_guid: str) -> EnvelopeStream: + url = "%s/apps/%s/stream" % (self.ws_doppler_endpoint, app_guid) + with WebsocketFrameReader( + url, + lambda: self.credentials_manager._access_token, + verify_ssl=self.verify_ssl, + proxy_host=self.proxy_host, + proxy_port=self.proxy_port, + proxy_auth=self.proxy_auth, + ) as websocket: for message in websocket: yield DopplerClient._parse_envelope(message) @staticmethod - def _parse_envelope(raw): + def _parse_envelope(raw) -> Envelope: envelope = Envelope() envelope.ParseFromString(raw) return envelope @staticmethod - def _extract_boundary(response): - content_type = response.headers['content-type'] - _logger.debug('content-type=%s' % content_type) - boundary_field = 'boundary=' + def _extract_boundary(response: Response) -> str: + content_type = response.headers["content-type"] + _logger.debug("content-type=%s" % content_type) + boundary_field = "boundary=" idx = content_type.find(boundary_field) if idx == -1: _logger.debug(response.text) - raise InvalidLogResponseException('Cannot extract boundary in %s' % content_type) - boundary = content_type[idx + len(boundary_field):] - idx = boundary.find(' ') + raise InvalidLogResponseException("Cannot extract boundary in %s" % content_type) + boundary = content_type[idx + len(boundary_field) :] + idx = boundary.find(" ") if idx != -1: boundary = boundary[:idx] return boundary @staticmethod def _read_multi_part_response(iterable, boundary): - remaining = '' - boundary_header = bufferize_string('--%s' % boundary) - end_of_line = bufferize_string('\r\n') + remaining = "" + boundary_header = bytes("--%s" % boundary, "UTF-8") + end_of_line = bytes("\r\n", "UTF-8") cpt_read = 0 for chunk_data in iterable: # _logger.debug('reading %d bytes' % size) @@ -88,11 +101,11 @@ def _read_multi_part_response(iterable, boundary): while part.find(end_of_line, 0, 2) == 0: part = part[2:] while part.rfind(end_of_line, len(part) - 2) == (len(part) - 2): - part = part[0:len(part) - 2] + part = part[0 : len(part) - 2] yield part - work = work[idx + len(boundary_header):] - if work[0] == '-' and work[1] == '-': - _logger.debug('end boundary reached') + work = work[idx + len(boundary_header) :] + if work[0] == "-" and work[1] == "-": + _logger.debug("end boundary reached") return else: idx = work.find(boundary_header) diff --git a/main/cloudfoundry_client/doppler/websocket_envelope_reader.py b/cloudfoundry_client/doppler/websocket_envelope_reader.py similarity index 57% rename from main/cloudfoundry_client/doppler/websocket_envelope_reader.py rename to cloudfoundry_client/doppler/websocket_envelope_reader.py index f96cd12..d4e63b3 100644 --- a/main/cloudfoundry_client/doppler/websocket_envelope_reader.py +++ b/cloudfoundry_client/doppler/websocket_envelope_reader.py @@ -1,10 +1,19 @@ import ssl +from collections.abc import Callable import websocket class WebsocketFrameReader(object): - def __init__(self, url, access_token_provider, verify_ssl=True, proxy_host=None, proxy_port=None): + def __init__( + self, + url, + access_token_provider: Callable[[], str], + verify_ssl: bool = True, + proxy_host: str | None = None, + proxy_port: int | None = None, + proxy_auth: tuple[str, str] | None = None, + ): if not verify_ssl: self._ws = websocket.WebSocket(sslopt=dict(cert_reqs=ssl.CERT_NONE)) else: @@ -12,13 +21,15 @@ def __init__(self, url, access_token_provider, verify_ssl=True, proxy_host=None, self._url = url self._proxy_host = proxy_host self._proxy_port = proxy_port + self._proxy_auth = proxy_auth self._access_token_provider = access_token_provider def connect(self): - kw_args = dict(header=dict(Authorization='Bearer %s' % self._access_token_provider())) + kw_args = dict(header=dict(Authorization="Bearer %s" % self._access_token_provider())) if self._proxy_host is not None and self._proxy_port is not None: - kw_args['http_proxy_host'] = self._proxy_host - kw_args['http_proxy_port'] = str(self._proxy_port) + kw_args["http_proxy_host"] = self._proxy_host + kw_args["http_proxy_port"] = str(self._proxy_port) + kw_args["http_proxy_auth"] = self._proxy_auth self._ws.connect(self._url, **kw_args) def close(self): @@ -36,5 +47,5 @@ def __iter__(self): try: for frame in self._ws: yield frame - except websocket.WebSocketConnectionClosedException as _: - pass \ No newline at end of file + except websocket.WebSocketConnectionClosedException: + pass diff --git a/cloudfoundry_client/dropsonde/envelope_pb2.py b/cloudfoundry_client/dropsonde/envelope_pb2.py new file mode 100644 index 0000000..5dcb8f4 --- /dev/null +++ b/cloudfoundry_client/dropsonde/envelope_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: envelope.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import cloudfoundry_client.dropsonde.http_pb2 as http__pb2 +import cloudfoundry_client.dropsonde.log_pb2 as log__pb2 +import cloudfoundry_client.dropsonde.metric_pb2 as metric__pb2 +import cloudfoundry_client.dropsonde.error_pb2 as error__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x65nvelope.proto\x12\x06\x65vents\x1a\nhttp.proto\x1a\tlog.proto\x1a\x0cmetric.proto\x1a\x0b\x65rror.proto\"\xab\x05\n\x08\x45nvelope\x12\x0e\n\x06origin\x18\x01 \x02(\t\x12-\n\teventType\x18\x02 \x02(\x0e\x32\x1a.events.Envelope.EventType\x12\x11\n\ttimestamp\x18\x06 \x01(\x03\x12\x12\n\ndeployment\x18\r \x01(\t\x12\x0b\n\x03job\x18\x0e \x01(\t\x12\r\n\x05index\x18\x0f \x01(\t\x12\n\n\x02ip\x18\x10 \x01(\t\x12(\n\x04tags\x18\x11 \x03(\x0b\x32\x1a.events.Envelope.TagsEntry\x12,\n\rhttpStartStop\x18\x07 \x01(\x0b\x32\x15.events.HttpStartStop\x12&\n\nlogMessage\x18\x08 \x01(\x0b\x32\x12.events.LogMessage\x12(\n\x0bvalueMetric\x18\t \x01(\x0b\x32\x13.events.ValueMetric\x12*\n\x0c\x63ounterEvent\x18\n \x01(\x0b\x32\x14.events.CounterEvent\x12\x1c\n\x05\x65rror\x18\x0b \x01(\x0b\x32\r.events.Error\x12\x30\n\x0f\x63ontainerMetric\x18\x0c \x01(\x0b\x32\x17.events.ContainerMetric\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x97\x01\n\tEventType\x12\x11\n\rHttpStartStop\x10\x04\x12\x0e\n\nLogMessage\x10\x05\x12\x0f\n\x0bValueMetric\x10\x06\x12\x10\n\x0c\x43ounterEvent\x10\x07\x12\t\n\x05\x45rror\x10\x08\x12\x13\n\x0f\x43ontainerMetric\x10\t\"\x04\x08\x01\x10\x03*\tHeartbeat*\tHttpStart*\x08HttpStopJ\x04\x08\x03\x10\x06R\tHeartbeatR\tHttpStartR\x08HttpStopBZ\n!org.cloudfoundry.dropsonde.eventsB\x0c\x45ventFactoryZ\'github.com/cloudfoundry/sonde-go/events') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'envelope_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n!org.cloudfoundry.dropsonde.eventsB\014EventFactoryZ\'github.com/cloudfoundry/sonde-go/events' + _ENVELOPE_TAGSENTRY._options = None + _ENVELOPE_TAGSENTRY._serialized_options = b'8\001' + _ENVELOPE._serialized_start=77 + _ENVELOPE._serialized_end=760 + _ENVELOPE_TAGSENTRY._serialized_start=525 + _ENVELOPE_TAGSENTRY._serialized_end=568 + _ENVELOPE_EVENTTYPE._serialized_start=571 + _ENVELOPE_EVENTTYPE._serialized_end=722 +# @@protoc_insertion_point(module_scope) diff --git a/cloudfoundry_client/dropsonde/error_pb2.py b/cloudfoundry_client/dropsonde/error_pb2.py new file mode 100644 index 0000000..b4ccaa6 --- /dev/null +++ b/cloudfoundry_client/dropsonde/error_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: error.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65rror.proto\x12\x06\x65vents\"6\n\x05\x45rror\x12\x0e\n\x06source\x18\x01 \x02(\t\x12\x0c\n\x04\x63ode\x18\x02 \x02(\x05\x12\x0f\n\x07message\x18\x03 \x02(\tBZ\n!org.cloudfoundry.dropsonde.eventsB\x0c\x45rrorFactoryZ\'github.com/cloudfoundry/sonde-go/events') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'error_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n!org.cloudfoundry.dropsonde.eventsB\014ErrorFactoryZ\'github.com/cloudfoundry/sonde-go/events' + _ERROR._serialized_start=23 + _ERROR._serialized_end=77 +# @@protoc_insertion_point(module_scope) diff --git a/cloudfoundry_client/dropsonde/http_pb2.py b/cloudfoundry_client/dropsonde/http_pb2.py new file mode 100644 index 0000000..d6b0eb2 --- /dev/null +++ b/cloudfoundry_client/dropsonde/http_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: http.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import cloudfoundry_client.dropsonde.uuid_pb2 as uuid__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nhttp.proto\x12\x06\x65vents\x1a\nuuid.proto\"\xff\x02\n\rHttpStartStop\x12\x16\n\x0estartTimestamp\x18\x01 \x02(\x03\x12\x15\n\rstopTimestamp\x18\x02 \x02(\x03\x12\x1f\n\trequestId\x18\x03 \x02(\x0b\x32\x0c.events.UUID\x12\"\n\x08peerType\x18\x04 \x02(\x0e\x32\x10.events.PeerType\x12\x1e\n\x06method\x18\x05 \x02(\x0e\x32\x0e.events.Method\x12\x0b\n\x03uri\x18\x06 \x02(\t\x12\x15\n\rremoteAddress\x18\x07 \x02(\t\x12\x11\n\tuserAgent\x18\x08 \x02(\t\x12\x12\n\nstatusCode\x18\t \x02(\x05\x12\x15\n\rcontentLength\x18\n \x02(\x03\x12#\n\rapplicationId\x18\x0c \x01(\x0b\x32\x0c.events.UUID\x12\x15\n\rinstanceIndex\x18\r \x01(\x05\x12\x12\n\ninstanceId\x18\x0e \x01(\t\x12\x11\n\tforwarded\x18\x0f \x03(\tJ\x04\x08\x0b\x10\x0cR\x0fparentRequestId*\"\n\x08PeerType\x12\n\n\x06\x43lient\x10\x01\x12\n\n\x06Server\x10\x02*\xc6\x04\n\x06Method\x12\x07\n\x03GET\x10\x01\x12\x08\n\x04POST\x10\x02\x12\x07\n\x03PUT\x10\x03\x12\n\n\x06\x44\x45LETE\x10\x04\x12\x08\n\x04HEAD\x10\x05\x12\x07\n\x03\x41\x43L\x10\x06\x12\x14\n\x10\x42\x41SELINE_CONTROL\x10\x07\x12\x08\n\x04\x42IND\x10\x08\x12\x0b\n\x07\x43HECKIN\x10\t\x12\x0c\n\x08\x43HECKOUT\x10\n\x12\x0b\n\x07\x43ONNECT\x10\x0b\x12\x08\n\x04\x43OPY\x10\x0c\x12\t\n\x05\x44\x45\x42UG\x10\r\x12\t\n\x05LABEL\x10\x0e\x12\x08\n\x04LINK\x10\x0f\x12\x08\n\x04LOCK\x10\x10\x12\t\n\x05MERGE\x10\x11\x12\x0e\n\nMKACTIVITY\x10\x12\x12\x0e\n\nMKCALENDAR\x10\x13\x12\t\n\x05MKCOL\x10\x14\x12\x11\n\rMKREDIRECTREF\x10\x15\x12\x0f\n\x0bMKWORKSPACE\x10\x16\x12\x08\n\x04MOVE\x10\x17\x12\x0b\n\x07OPTIONS\x10\x18\x12\x0e\n\nORDERPATCH\x10\x19\x12\t\n\x05PATCH\x10\x1a\x12\x07\n\x03PRI\x10\x1b\x12\x0c\n\x08PROPFIND\x10\x1c\x12\r\n\tPROPPATCH\x10\x1d\x12\n\n\x06REBIND\x10\x1e\x12\n\n\x06REPORT\x10\x1f\x12\n\n\x06SEARCH\x10 \x12\x0e\n\nSHOWMETHOD\x10!\x12\r\n\tSPACEJUMP\x10\"\x12\x0e\n\nTEXTSEARCH\x10#\x12\t\n\x05TRACE\x10$\x12\t\n\x05TRACK\x10%\x12\n\n\x06UNBIND\x10&\x12\x0e\n\nUNCHECKOUT\x10\'\x12\n\n\x06UNLINK\x10(\x12\n\n\x06UNLOCK\x10)\x12\n\n\x06UPDATE\x10*\x12\x15\n\x11UPDATEREDIRECTREF\x10+\x12\x13\n\x0fVERSION_CONTROL\x10,BY\n!org.cloudfoundry.dropsonde.eventsB\x0bHttpFactoryZ\'github.com/cloudfoundry/sonde-go/events') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'http_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n!org.cloudfoundry.dropsonde.eventsB\013HttpFactoryZ\'github.com/cloudfoundry/sonde-go/events' + _PEERTYPE._serialized_start=420 + _PEERTYPE._serialized_end=454 + _METHOD._serialized_start=457 + _METHOD._serialized_end=1039 + _HTTPSTARTSTOP._serialized_start=35 + _HTTPSTARTSTOP._serialized_end=418 +# @@protoc_insertion_point(module_scope) diff --git a/cloudfoundry_client/dropsonde/log_pb2.py b/cloudfoundry_client/dropsonde/log_pb2.py new file mode 100644 index 0000000..3c76fbd --- /dev/null +++ b/cloudfoundry_client/dropsonde/log_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: log.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tlog.proto\x12\x06\x65vents\"\xc5\x01\n\nLogMessage\x12\x0f\n\x07message\x18\x01 \x02(\x0c\x12\x34\n\x0cmessage_type\x18\x02 \x02(\x0e\x32\x1e.events.LogMessage.MessageType\x12\x11\n\ttimestamp\x18\x03 \x02(\x03\x12\x0e\n\x06\x61pp_id\x18\x04 \x01(\t\x12\x13\n\x0bsource_type\x18\x05 \x01(\t\x12\x17\n\x0fsource_instance\x18\x06 \x01(\t\"\x1f\n\x0bMessageType\x12\x07\n\x03OUT\x10\x01\x12\x07\n\x03\x45RR\x10\x02\x42X\n!org.cloudfoundry.dropsonde.eventsB\nLogFactoryZ\'github.com/cloudfoundry/sonde-go/events') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'log_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n!org.cloudfoundry.dropsonde.eventsB\nLogFactoryZ\'github.com/cloudfoundry/sonde-go/events' + _LOGMESSAGE._serialized_start=22 + _LOGMESSAGE._serialized_end=219 + _LOGMESSAGE_MESSAGETYPE._serialized_start=188 + _LOGMESSAGE_MESSAGETYPE._serialized_end=219 +# @@protoc_insertion_point(module_scope) diff --git a/cloudfoundry_client/dropsonde/metric_pb2.py b/cloudfoundry_client/dropsonde/metric_pb2.py new file mode 100644 index 0000000..49b8d4f --- /dev/null +++ b/cloudfoundry_client/dropsonde/metric_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: metric.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cmetric.proto\x12\x06\x65vents\"8\n\x0bValueMetric\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\r\n\x05value\x18\x02 \x02(\x01\x12\x0c\n\x04unit\x18\x03 \x02(\t\":\n\x0c\x43ounterEvent\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\r\n\x05\x64\x65lta\x18\x02 \x02(\x04\x12\r\n\x05total\x18\x03 \x01(\x04\"\xb0\x01\n\x0f\x43ontainerMetric\x12\x15\n\rapplicationId\x18\x01 \x02(\t\x12\x15\n\rinstanceIndex\x18\x02 \x02(\x05\x12\x15\n\rcpuPercentage\x18\x03 \x02(\x01\x12\x13\n\x0bmemoryBytes\x18\x04 \x02(\x04\x12\x11\n\tdiskBytes\x18\x05 \x02(\x04\x12\x18\n\x10memoryBytesQuota\x18\x06 \x01(\x04\x12\x16\n\x0e\x64iskBytesQuota\x18\x07 \x01(\x04\x42[\n!org.cloudfoundry.dropsonde.eventsB\rMetricFactoryZ\'github.com/cloudfoundry/sonde-go/events') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'metric_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n!org.cloudfoundry.dropsonde.eventsB\rMetricFactoryZ\'github.com/cloudfoundry/sonde-go/events' + _VALUEMETRIC._serialized_start=24 + _VALUEMETRIC._serialized_end=80 + _COUNTEREVENT._serialized_start=82 + _COUNTEREVENT._serialized_end=140 + _CONTAINERMETRIC._serialized_start=143 + _CONTAINERMETRIC._serialized_end=319 +# @@protoc_insertion_point(module_scope) diff --git a/cloudfoundry_client/dropsonde/uuid_pb2.py b/cloudfoundry_client/dropsonde/uuid_pb2.py new file mode 100644 index 0000000..fc002ea --- /dev/null +++ b/cloudfoundry_client/dropsonde/uuid_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: uuid.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nuuid.proto\x12\x06\x65vents\"!\n\x04UUID\x12\x0b\n\x03low\x18\x01 \x02(\x04\x12\x0c\n\x04high\x18\x02 \x02(\x04\x42Y\n!org.cloudfoundry.dropsonde.eventsB\x0bUuidFactoryZ\'github.com/cloudfoundry/sonde-go/events') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'uuid_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n!org.cloudfoundry.dropsonde.eventsB\013UuidFactoryZ\'github.com/cloudfoundry/sonde-go/events' + _UUID._serialized_start=22 + _UUID._serialized_end=55 +# @@protoc_insertion_point(module_scope) diff --git a/cloudfoundry_client/errors.py b/cloudfoundry_client/errors.py new file mode 100644 index 0000000..41f8910 --- /dev/null +++ b/cloudfoundry_client/errors.py @@ -0,0 +1,32 @@ +import json +from http import HTTPStatus + + +class InvalidLogResponseException(Exception): + pass + + +class InvalidStatusCode(Exception): + def __init__(self, status_code: HTTPStatus, body, request_id=None): + self.status_code = status_code + self.body = body + self.request_id = request_id + + def __str__(self): + error_message = self.status_code.name + if isinstance(self.body, str): + error_message += f" = {self.body}" + else: + error_message += f" = {json.dumps(self.body)}" + if self.request_id: + error_message += f" - vcap-request-id = {self.request_id}" + return error_message + + +class InvalidEntity(Exception): + def __init__(self, **kwargs): + super().__init__() + self.raw_entity = dict(**kwargs) + + def __str__(self): + return "InvalidEntity: %s" % json.dumps(self.raw_entity) diff --git a/main/cloudfoundry_client/doppler/__init__.py b/cloudfoundry_client/main/__init__.py similarity index 100% rename from main/cloudfoundry_client/doppler/__init__.py rename to cloudfoundry_client/main/__init__.py diff --git a/cloudfoundry_client/main/apps_command_domain.py b/cloudfoundry_client/main/apps_command_domain.py new file mode 100644 index 0000000..641c6f4 --- /dev/null +++ b/cloudfoundry_client/main/apps_command_domain.py @@ -0,0 +1,112 @@ +from argparse import _SubParsersAction, Namespace +from collections.abc import Callable + +from cloudfoundry_client.client import CloudFoundryClient +from cloudfoundry_client.main.command_domain import CommandDomain, Command + + +class AppCommandDomain(CommandDomain): + def __init__(self): + super().__init__( + display_name="Applications", + entity_name="app", + filter_list_parameters=["organization_guid", "space_guid"], + allow_retrieve_by_name=True, + allow_deletion=True, + extra_methods=[ + ( + self.recent_logs(), + "Recent Logs", + ), + ( + self.stream_logs(), + "Stream Logs", + ), + (self.simple_extra_command("env"), "Get the environment of an application"), + ( + self.simple_extra_command("instances"), + "Get the instances of an application", + ), + ( + self.simple_extra_command("stats"), + "Get the stats of an application", + ), + ( + self.simple_extra_command("summary"), + "Get the summary of an application", + ), + ( + self.simple_extra_command("start"), + "Start an application", + ), + ( + self.simple_extra_command("stop"), + "Stop an application", + ), + ( + self.simple_extra_command("restage"), + "Restage an application", + ), + (self.app_routes(), "List the routes(host) of an application"), + (self.restart_instance(), "Restart the instance of an application"), + ], + ) + + def recent_logs(self) -> Command: + def execute(client, arguments): + resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) + for envelope in client.doppler.recent_logs(resource_id): + print(envelope) + + return Command("recent_logs", self._generate_id_command_parser("recent_logs"), execute) + + def stream_logs(self) -> Command: + def execute(client, arguments): + resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) + try: + for envelope in client.doppler.stream_logs(resource_id): + print(envelope) + except KeyboardInterrupt: + pass + + return Command("stream_logs", self._generate_id_command_parser("stream_logs"), execute) + + def simple_extra_command(self, entry) -> Command: + def execute(client, arguments): + resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) + print(getattr(self._get_client_domain(client), entry)(resource_id).json(indent=1)) + + return Command(entry, self._generate_id_command_parser(entry), execute) + + def app_routes(self) -> Command: + def execute(client: CloudFoundryClient, arguments: Namespace): + resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) + for entity in getattr(self._get_client_domain(client), "list_routes")(resource_id): + print("%s - %s" % (entity["metadata"]["guid"], entity["entity"]["host"])) + + return Command("app_routes", self._generate_id_command_parser("app_routes"), execute) + + def restart_instance(self) -> Command: + def generate_parser(parser: _SubParsersAction): + command_parser = parser.add_parser("restart_instance") + command_parser.add_argument( + "id", metavar="ids", type=str, nargs=1, help="The id. Can be UUID or name (first found then)" + ) + command_parser.add_argument("instance_id", metavar="instance_ids", type=int, nargs=1, help="The instance id") + + def execute(client: CloudFoundryClient, arguments: Namespace): + app_domain = self._get_client_domain(client) + resource_id = self.resolve_id(arguments.id[0], lambda x: app_domain.get_first(name=x)) + getattr(app_domain, "restart_instance")(resource_id, int(arguments.instance_id[0])) + + return Command("restart_instance", generate_parser, execute) + + @staticmethod + def _generate_id_command_parser(entry: str) -> Callable[[_SubParsersAction], None]: + def generate_parser(parser: _SubParsersAction): + command_parser = parser.add_parser(entry) + command_parser.add_argument( + "id", metavar="ids", type=str, nargs=1, help="The id. Can be UUID or name (first found then)" + ) + + return generate_parser diff --git a/cloudfoundry_client/main/command_domain.py b/cloudfoundry_client/main/command_domain.py new file mode 100644 index 0000000..0910975 --- /dev/null +++ b/cloudfoundry_client/main/command_domain.py @@ -0,0 +1,249 @@ +import functools +import json +import os +import re +from argparse import _SubParsersAction, Namespace +from collections import OrderedDict +from collections.abc import Callable +from http import HTTPStatus +from typing import Any + +from cloudfoundry_client.client import CloudFoundryClient +from cloudfoundry_client.errors import InvalidStatusCode +from cloudfoundry_client.common_objects import JsonObject + + +class Command(object): + def __init__( + self, + entry: str, + generate_parser: Callable[[_SubParsersAction], Any], + execute: Callable[[CloudFoundryClient, Namespace], Any], + ): + self.entry = entry + self.generate_parser = generate_parser + self.execute = execute + + +class CommandDomain(object): + def __init__( + self, + display_name: str, + entity_name: str, + filter_list_parameters: list, + api_version: str = "v2", + name_property: str = "name", + allow_retrieve_by_name: bool = False, + allow_creation: bool = False, + allow_deletion: bool = False, + extra_methods: list = None, + ): + self.display_name = display_name + self.client_domain = self.plural(entity_name) + self.api_version = api_version + self.entity_name = entity_name + self.filter_list_parameters = filter_list_parameters + self.name_property = name_property + self.allow_retrieve_by_name = allow_retrieve_by_name + self.allow_creation = allow_creation + self.allow_deletion = allow_deletion + + self.commands = OrderedDict() + self.commands[self._list_entry()] = self.list() + self.commands[self._get_entry()] = self.get() + if self.allow_creation: + self.commands[self._create_entry()] = self.create() + if self.allow_deletion: + self.commands[self._delete_entry()] = self.delete() + self.extra_description = OrderedDict() + if extra_methods is not None: + for command in extra_methods: + self.commands[command[0].entry] = command[0] + self.extra_description[command[0].entry] = command[1] + + def description(self) -> list[str]: + description = [ + " %s" % self.display_name, + " %s : List %ss" % (self._list_entry(), self.entity_name), + " %s : Get a %s by %s" + % (self._get_entry(), self.entity_name, "UUID or name (first found then)" if self.allow_retrieve_by_name else "UUID"), + ] + if self.allow_creation: + description.append(" %s : Create a %s" % (self._create_entry(), self.entity_name)) + + if self.allow_deletion: + description.append(" %s : Delete a %s" % (self._delete_entry(), self.entity_name)) + description.extend([" %s : %s" % (k, v) for k, v in self.extra_description.items()]) + return description + + def generate_parser(self, parser: _SubParsersAction): + for command in self.commands.values(): + command.generate_parser(parser) + + def is_handled(self, action: str) -> bool: + return action in self.commands + + def execute(self, client: CloudFoundryClient, action: str, arguments: Namespace): + return self.commands[action].execute(client, arguments) + + def _get_client_domain(self, client: CloudFoundryClient) -> Any: + return getattr(getattr(client, self.api_version), self.client_domain) + + @staticmethod + def plural(entity_name: str) -> str: + if entity_name.endswith("y") and not (re.match(r".+[aeiou]y", entity_name)): + return "%sies" % entity_name[: len(entity_name) - 1] + else: + return "%ss" % entity_name + + @staticmethod + def is_guid(s: str) -> bool: + return re.match(r"[\d|a-z]{8}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{12}", s.lower()) is not None + + def id(self, entity: JsonObject) -> str: + if self.api_version == "v2": + return entity["metadata"]["guid"] + elif self.api_version == "v3": + return entity["guid"] + + def resolve_id(self, argument: str, get_by_name: Callable[[str], JsonObject]) -> str: + if CommandDomain.is_guid(argument): + return argument + elif self.allow_retrieve_by_name: + result = get_by_name(argument) + if result is not None: + if self.api_version == "v2": + return result["metadata"]["guid"] + elif self.api_version == "v3": + return result["guid"] + else: + raise InvalidStatusCode(HTTPStatus.NOT_FOUND, "%s with name %s" % (self.client_domain, argument)) + else: + raise ValueError("id: %s: does not allow search by name" % self.client_domain) + + def name(self, entity: JsonObject) -> str: + if self.api_version == "v2": + return entity["entity"][self.name_property] + elif self.api_version == "v3": + return entity[self.name_property] + + def find_by_name(self, client: CloudFoundryClient, name: str) -> JsonObject: + return self._get_client_domain(client).get_first(**{self.name_property: name}) + + def create(self) -> Command: + entry = self._create_entry() + + def execute(client: CloudFoundryClient, arguments: Namespace): + data = None + if os.path.isfile(arguments.entity[0]): + with open(arguments.entity[0], "r") as f: + try: + data = json.load(f) + except ValueError: + raise ValueError("entity: file %s does not contain valid json data" % arguments.entity[0]) + else: + try: + data = json.loads(arguments.entity[0]) + except ValueError: + raise ValueError("entity: must be either a valid json file path or a json object") + print(self._get_client_domain(client)._create(data).json()) + + def generate_parser(parser: _SubParsersAction): + create_parser = parser.add_parser(entry) + create_parser.add_argument( + "entity", + metavar="entities", + type=str, + nargs=1, + help="Either a path of the json file containing the %s or a json object or the json %s object" + % (self.client_domain, self.client_domain), + ) + + return Command(entry, generate_parser, execute) + + def delete(self) -> Command: + entry = self._delete_entry() + + def execute(client: CloudFoundryClient, arguments: Namespace): + if self.is_guid(arguments.id[0]): + self._get_client_domain(client)._remove(arguments.id[0]) + elif self.allow_retrieve_by_name: + entity = self.find_by_name(client, arguments.id[0]) + if entity is None: + raise InvalidStatusCode(HTTPStatus.NOT_FOUND, "%s with name %s" % (self.client_domain, arguments.id[0])) + else: + self._get_client_domain(client)._remove(self.id(entity)) + else: + raise ValueError("id: %s: does not allow search by name" % self.client_domain) + + def generate_parser(parser: _SubParsersAction): + delete_parser = parser.add_parser(entry) + delete_parser.add_argument( + "id", + metavar="ids", + type=str, + nargs=1, + help="The id. Can be UUID or name (first found then)" if self.allow_retrieve_by_name else "The id (UUID)", + ) + + return Command(entry, generate_parser, execute) + + def get(self) -> Command: + entry = self._get_entry() + + def execute(client: CloudFoundryClient, arguments: Namespace): + resource_id = self.resolve_id(arguments.id[0], functools.partial(self.find_by_name, client)) + print(self._get_client_domain(client).get(resource_id).json(indent=1)) + + def generate_parser(parser: _SubParsersAction): + get_parser = parser.add_parser(entry) + get_parser.add_argument( + "id", + metavar="ids", + type=str, + nargs=1, + help="The id. Can be UUID or name (first found then)" if self.allow_retrieve_by_name else "The id (UUID)", + ) + + return Command(entry, generate_parser, execute) + + def list(self) -> Command: + entry = self._list_entry() + + def execute(client: CloudFoundryClient, arguments: Namespace): + filter_list = dict() + for filter_parameter in self.filter_list_parameters: + filter_value = getattr(arguments, filter_parameter) + if filter_value is not None: + filter_list[filter_parameter] = filter_value + for entity in self._get_client_domain(client).list(**filter_list): + if self.name_property is not None: + print("%s - %s" % (self.id(entity), self.name(entity))) + else: + print(self.id(entity)) + + def generate_parser(parser: _SubParsersAction): + list_parser = parser.add_parser(entry) + for filter_parameter in self.filter_list_parameters: + list_parser.add_argument( + "-%s" % filter_parameter, + action="store", + dest=filter_parameter, + type=str, + default=None, + help="Filter with %s" % filter_parameter, + ) + + return Command(entry, generate_parser, execute) + + def _list_entry(self) -> str: + return "list_%s" % self.plural(self.entity_name) + + def _create_entry(self) -> str: + return "create_%s" % self.entity_name + + def _delete_entry(self) -> str: + return "delete_%s" % self.entity_name + + def _get_entry(self) -> str: + return "get_%s" % self.entity_name diff --git a/cloudfoundry_client/main/main.py b/cloudfoundry_client/main/main.py new file mode 100644 index 0000000..6d5b4b7 --- /dev/null +++ b/cloudfoundry_client/main/main.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +import argparse +import json +import logging +import os +import re +import sys +from collections.abc import Callable +from http import HTTPStatus +from typing import Any + +from requests.exceptions import ConnectionError + +from cloudfoundry_client import __version__ +from cloudfoundry_client.client import CloudFoundryClient +from cloudfoundry_client.errors import InvalidStatusCode +from cloudfoundry_client.common_objects import JsonObject +from cloudfoundry_client.main.apps_command_domain import AppCommandDomain +from cloudfoundry_client.main.command_domain import CommandDomain, Command +from cloudfoundry_client.main.operation_commands import generate_push_command +from cloudfoundry_client.main.tasks_command_domain import TaskCommandDomain + +__all__ = ["main", "build_client_from_configuration"] + +_logger = logging.getLogger(__name__) + + +def _read_value_from_user( + prompt: str, error_message: str = None, validator: Callable[[str], bool] = None, default: str = "" +) -> str: + while True: + sys.stdout.write("%s [%s]: " % (prompt, default)) + sys.stdout.flush() + answer_value = sys.stdin.readline().rstrip(" \r\n") + if len(answer_value) == 0: + answer_value = default + if len(answer_value) > 0 and (validator is None or validator(answer_value)): + return answer_value + else: + if error_message is None: + sys.stderr.write('"%s": invalid value\n' % answer_value) + else: + sys.stderr.write('"%s": %s\n' % (answer_value, error_message)) + + +def get_user_directory() -> str: + dir_conf = os.path.join(os.path.expanduser("~")) + if not os.path.isdir(dir_conf): + if os.path.exists(dir_conf): + raise IOError("%s exists but is not a directory") + os.mkdir(dir_conf) + return dir_conf + + +def get_config_file() -> str: + return os.path.join(get_user_directory(), ".cf_client_python.json") + + +def import_from_clf_cli(): + user_directory = get_user_directory() + cf_cli_dir = os.path.join(user_directory, ".cf") + if not os.path.isdir(cf_cli_dir): + raise IOError("%s directory not found" % cf_cli_dir) + config_file = os.path.join(cf_cli_dir, "config.json") + if not os.path.isfile(config_file): + raise IOError("%s not found" % config_file) + with open(config_file, "r") as cf_cli_file: + cf_cli_data = json.load(cf_cli_file) + if cf_cli_data["RefreshToken"] is None or cf_cli_data["Target"] is None: + raise IOError("Could not load informations from cf cli configuration") + with open(get_config_file(), "w") as f: + f.write( + json.dumps( + dict(target_endpoint=cf_cli_data["Target"], verify=False, refresh_token=cf_cli_data["RefreshToken"]), indent=2 + ) + ) + + +def build_client_from_configuration(previous_configuration: dict = None) -> CloudFoundryClient: + config_file = get_config_file() + if not os.path.isfile(config_file): + target_endpoint = _read_value_from_user( + "Please enter a target endpoint", + "Url must starts with http:// or https://", + lambda s: s.startswith("http://") or s.startswith("https://"), + default="" if previous_configuration is None else previous_configuration.get("target_endpoint", ""), + ) + verify = _read_value_from_user( + "Verify ssl (true/false)", + "Enter either true or false", + lambda s: s == "true" or s == "false", + default="true" if previous_configuration is None else json.dumps(previous_configuration.get("verify", True)), + ) + login = _read_value_from_user("Please enter your login") + password = _read_value_from_user("Please enter your password") + client = CloudFoundryClient(target_endpoint, verify=(verify == "true")) + client.init_with_user_credentials(login, password) + with open(config_file, "w") as f: + f.write( + json.dumps( + dict(target_endpoint=target_endpoint, verify=(verify == "true"), refresh_token=client.refresh_token), indent=2 + ) + ) + return client + else: + try: + configuration = None + with open(config_file, "r") as f: + configuration = json.load(f) + client = CloudFoundryClient(configuration["target_endpoint"], verify=configuration["verify"]) + client.init_with_token(configuration["refresh_token"]) + return client + except Exception as ex: + if isinstance(ex, ConnectionError): + raise + else: + _logger.exception("Could not restore configuration. Cleaning and recreating") + os.remove(config_file) + return build_client_from_configuration(configuration) + + +def is_guid(s: str) -> bool: + return re.match(r"[\d|a-z]{8}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{12}", s.lower()) is not None + + +def resolve_id(argument: str, get_by_name: Callable[[str], JsonObject], domain_name: str, allow_search_by_name: bool) -> str: + if is_guid(argument): + return argument + elif allow_search_by_name: + result = get_by_name(argument) + if result is not None: + return result["metadata"]["guid"] + else: + raise InvalidStatusCode(HTTPStatus.NOT_FOUND, "%s with name %s" % (domain_name, argument)) + else: + raise ValueError("id: %s: does not allow search by name" % domain_name) + + +def log_recent(client: CloudFoundryClient, application_guid: str): + for envelope in client.doppler.recent_logs(application_guid): + _logger.info(envelope) + + +def stream_logs(client: CloudFoundryClient, application_guid: str): + try: + for envelope in client.doppler.stream_logs(application_guid): + _logger.info(envelope) + except KeyboardInterrupt: + pass + + +def _get_v2_client_domain(client: CloudFoundryClient, domain: str) -> Any: + return getattr(client.v2, "%ss" % domain) + + +def generate_oauth_token_command() -> tuple[Command, str]: + entry = "oauth-token" + + def generate_parser(parser: argparse._SubParsersAction): + parser.add_parser(entry) + + def execute(client: CloudFoundryClient, arguments: argparse.Namespace): + token = client._access_token + print(token if token is not None else "No token") + + return Command(entry, generate_parser, execute), "Display oauth token" + + +def main(): + logging.basicConfig(level=logging.INFO, format="%(message)s") + logging.getLogger("requests").setLevel(logging.WARNING) + logging.getLogger("urllib3").setLevel(logging.WARNING) + + commands = [ + CommandDomain( + display_name="Organizations", + entity_name="organization", + api_version="v3", + filter_list_parameters=["names", "guids"], + allow_retrieve_by_name=True, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="OrganizationQuotas", + entity_name="organization_quota", + api_version="v3", + filter_list_parameters=["names", "guids", "organization_guids"], + allow_retrieve_by_name=True, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Spaces", + entity_name="space", + filter_list_parameters=["organization_guid"], + allow_retrieve_by_name=True, + allow_creation=True, + allow_deletion=True, + ), + AppCommandDomain(), + CommandDomain( + display_name="Services", + entity_name="service", + filter_list_parameters=["service_broker_guid"], + name_property="label", + allow_retrieve_by_name=True, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Service Plans", + entity_name="service_plan", + filter_list_parameters=["service_guid", "service_instance_guid", "service_broker_guid"], + ), + CommandDomain( + display_name="Service Instances", + entity_name="service_instance", + filter_list_parameters=["organization_guid", "space_guid", "service_plan_guid"], + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Service Keys", + entity_name="service_key", + filter_list_parameters=["service_instance_guid"], + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Service Bindings", + entity_name="service_binding", + filter_list_parameters=["app_guid", "service_instance_guid"], + name_property=None, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Service Broker", + entity_name="service_broker", + filter_list_parameters=["name", "space_guid"], + allow_retrieve_by_name=True, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Service Plan Visibilities", + entity_name="service_plan_visibility", + filter_list_parameters=["organization_guid", "service_plan_guid"], + name_property=None, + allow_retrieve_by_name=False, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Buildpacks", + entity_name="buildpack", + api_version="v3", + filter_list_parameters=["names", "stacks"], + allow_retrieve_by_name=True, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain( + display_name="Domains", + entity_name="domain", + api_version="v3", + filter_list_parameters=[], + allow_retrieve_by_name=True, + allow_creation=True, + allow_deletion=True, + ), + CommandDomain(display_name="Routes", entity_name="route", name_property="host", filter_list_parameters=[]), + TaskCommandDomain(), + ] + operation_commands = [generate_push_command()] + others_commands = [generate_oauth_token_command()] + + descriptions = [] + for command in commands: + descriptions.extend(command.description()) + + descriptions.append("Operations") + for command, description in operation_commands: + descriptions.append(" %s: %s" % (command.entry, description)) + + descriptions.append("Others") + for command, description in others_commands: + descriptions.append(" %s: %s" % (command.entry, description)) + + parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("-V", "--version", action="version", version=__version__) + subparsers = parser.add_subparsers(title="Commands", dest="action", description="\n".join(descriptions)) + subparsers.add_parser("import_from_cf_cli", help="Copy CF CLI configuration into our configuration") + + for command in commands: + command.generate_parser(subparsers) + for other_command_domain in [operation_commands, others_commands]: + for command, _ in other_command_domain: + command.generate_parser(subparsers) + + arguments = parser.parse_args() + if arguments.action == "import_from_cf_cli": + import_from_clf_cli() + else: + client = build_client_from_configuration() + for command in commands: + if command.is_handled(arguments.action): + command.execute(client, arguments.action, arguments) + return + for other_command_domain in [operation_commands, others_commands]: + for command, _ in other_command_domain: + if command.entry == arguments.action: + command.execute(client, arguments) + return + + raise ValueError("Domain not found for action %s" % arguments.action) + + +if __name__ == "__main__": + main() diff --git a/cloudfoundry_client/main/operation_commands.py b/cloudfoundry_client/main/operation_commands.py new file mode 100644 index 0000000..89db16a --- /dev/null +++ b/cloudfoundry_client/main/operation_commands.py @@ -0,0 +1,20 @@ +from argparse import _SubParsersAction, Namespace + +from cloudfoundry_client.client import CloudFoundryClient +from cloudfoundry_client.main.command_domain import Command +from cloudfoundry_client.operations.push.push import PushOperation + + +def generate_push_command() -> tuple[Command, str]: + entry = "push_app" + + def generate_parser(parser: _SubParsersAction): + command_parser = parser.add_parser(entry) + command_parser.add_argument("manifest_path", metavar="manifest_paths", type=str, nargs=1, help="The manifest path") + command_parser.add_argument("-space_guid", action="store", dest="space_guid", type=str, help="Space guid") + + def execute(client: CloudFoundryClient, arguments: Namespace): + manifest_path = arguments.manifest_path[0] + PushOperation(client).push(arguments.space_guid, manifest_path) + + return Command(entry, generate_parser, execute), "Push an application by its manifest" diff --git a/cloudfoundry_client/main/tasks_command_domain.py b/cloudfoundry_client/main/tasks_command_domain.py new file mode 100644 index 0000000..8dcdbdc --- /dev/null +++ b/cloudfoundry_client/main/tasks_command_domain.py @@ -0,0 +1,78 @@ +import json +import os +from argparse import Namespace, _SubParsersAction + +from cloudfoundry_client.client import CloudFoundryClient +from cloudfoundry_client.common_objects import JsonObject +from cloudfoundry_client.main.command_domain import CommandDomain, Command + + +class TaskCommandDomain(CommandDomain): + def __init__(self): + super().__init__( + display_name="Tasks", + entity_name="task", + filter_list_parameters=["names", "app_guids", "space_guids", "organization_guids"], + api_version="v3", + allow_creation=True, + allow_deletion=False, + extra_methods=[ + ( + self.cancel(), + "Cancel Task", + ) + ], + ) + + def id(self, entity: JsonObject) -> str: + return entity["guid"] + + def name(self, entity: JsonObject) -> str: + return entity[self.name_property] + + def find_by_name(self, client: CloudFoundryClient, name: str): + return self._get_client_domain(client).get_first(**{"%ss" % self.name_property: name}) + + def create(self) -> Command: + entry = self._create_entry() + + def execute(client: CloudFoundryClient, arguments: Namespace): + data = None + if os.path.isfile(arguments.entity[0]): + with open(arguments.entity[0], "r") as f: + try: + data = json.load(f) + except ValueError: + raise ValueError("entity: file %s does not contain valid json data" % arguments.entity[0]) + else: + try: + data = json.loads(arguments.entity[0]) + except ValueError: + raise ValueError("entity: must be either a valid json file path or a json object") + print(self._get_client_domain(client).create(arguments.app_id[0], **data).json()) + + def generate_parser(parser: _SubParsersAction): + create_parser = parser.add_parser(entry) + create_parser.add_argument("app_id", metavar="ids", type=str, nargs=1, help="The application UUID.") + create_parser.add_argument( + "entity", + metavar="entities", + type=str, + nargs=1, + help="Either a path of the json file containing the %s or a json object or the json %s object" + % (self.client_domain, self.client_domain), + ) + + return Command(entry, generate_parser, execute) + + def cancel(self) -> Command: + entry = "cancel_task" + + def execute(client: CloudFoundryClient, arguments: Namespace): + print(self._get_client_domain(client).cancel(arguments.id[0]).json(indent=1)) + + def generate_parser(parser: _SubParsersAction): + command_parser = parser.add_parser(entry) + command_parser.add_argument("id", metavar="ids", type=str, nargs=1, help="The task UUID") + + return Command(entry, generate_parser, execute) diff --git a/main/cloudfoundry_client/dropsonde/__init__.py b/cloudfoundry_client/networking/__init__.py similarity index 100% rename from main/cloudfoundry_client/dropsonde/__init__.py rename to cloudfoundry_client/networking/__init__.py diff --git a/cloudfoundry_client/networking/entities.py b/cloudfoundry_client/networking/entities.py new file mode 100644 index 0000000..aebb2e7 --- /dev/null +++ b/cloudfoundry_client/networking/entities.py @@ -0,0 +1,128 @@ +import logging +from collections.abc import Callable, Generator +from functools import reduce +from typing import Any, TYPE_CHECKING +from urllib.parse import quote + +from requests import Response + +from cloudfoundry_client.errors import InvalidEntity +from cloudfoundry_client.common_objects import JsonObject, Request + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + +_logger = logging.getLogger(__name__) + + +class Entity(JsonObject): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient", *args, **kwargs): + super().__init__(*args, **kwargs) + self.target_endpoint = target_endpoint + self.client = client + try: + src = self["source"] + dst = self["destination"] + src["id"] + dst["id"] + dst["protocol"] + dst["ports"]["start"] + dst["ports"]["end"] + except KeyError: + raise InvalidEntity(**self) + + +EntityBuilder = Callable[[list[tuple[str, Any]]], Entity] + + +class EntityManager(object): + list_query_parameters = ["page", "results-per-page", "order-direction"] + + list_multi_parameters = ["order-by"] + + def __init__( + self, target_endpoint: str, client: "CloudFoundryClient", entity_uri: str, entity_builder: EntityBuilder | None = None + ): + self.target_endpoint = target_endpoint + self.entity_uri = entity_uri + self.client = client + self.entity_builder = ( + entity_builder if entity_builder is not None else lambda pairs: Entity(target_endpoint, client, pairs) + ) + + def _list( + self, requested_path: str, entity_builder: EntityBuilder | None = None, **kwargs + ) -> Generator[Entity, None, None]: + url_requested = self._get_url_filtered("%s%s" % (self.target_endpoint, requested_path), **kwargs) + response = self.client.get(url_requested) + entity_builder = self._get_entity_builder(entity_builder) + _logger.debug("GET - %s - %s", url_requested, response.text) + response_json = self._read_response(response, JsonObject) + for resource in response_json["policies"]: + yield entity_builder(list(resource.items())) + + def _create(self, data: dict, **kwargs) -> Entity: + url = "%s%s" % (self.target_endpoint, self.entity_uri) + return self._post(url, data, **kwargs) + + def _remove(self, resource_id: str, **kwargs): + url = "%s%s/%s" % (self.target_endpoint, self.entity_uri, resource_id) + self._delete(url, **kwargs) + + def _post(self, url: str, data: dict | None = None, **kwargs): + response = self.client.post(url, json=data, **kwargs) + _logger.debug("POST - %s - %s", url, response.text) + return self._read_response(response) + + def _delete(self, url: str, **kwargs): + response = self.client.delete(url, **kwargs) + _logger.debug("DELETE - %s - %s", url, response.text) + + def __iter__(self) -> Generator[Entity, None, None]: + return self.list() + + def list(self, **kwargs) -> Generator[Entity, None, None]: + return self._list(self.entity_uri, **kwargs) + + def get_first(self, **kwargs) -> Entity | None: + kwargs.setdefault("results-per-page", 1) + for entity in self._list(self.entity_uri, **kwargs): + return entity + return None + + def _read_response(self, response: Response, other_entity_builder: EntityBuilder | None = None): + entity_builder = self._get_entity_builder(other_entity_builder) + result = response.json(object_pairs_hook=JsonObject) + return entity_builder(list(result.items())) + + @staticmethod + def _request(**mandatory_parameters) -> Request: + return Request(**mandatory_parameters) + + def _get_entity_builder(self, entity_builder: EntityBuilder | None) -> EntityBuilder: + if entity_builder is None: + return self.entity_builder + else: + return entity_builder + + def _get_url_filtered(self, 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 parameter_name in self.list_query_parameters: + parameters.append("%s=%s" % (parameter_name, str(parameter_value))) + elif parameter_name in self.list_multi_parameters: + value_list = parameter_value + if not isinstance(value_list, (list, tuple)): + value_list = [value_list] + for value in value_list: + parameters.append("%s=%s" % (parameter_name, str(value))) + elif isinstance(parameter_value, (list, tuple)): + parameters.append("%s=%s" % (parameter_name, quote(",".join(parameter_value)))) + else: + parameters.append("%s=%s" % (parameter_name, quote(str(parameter_value)))) + return parameters + + if len(kwargs) > 0: + return "%s?%s" % (url, "&".join(reduce(_append_encoded_parameter, sorted(list(kwargs.items())), []))) + else: + return url diff --git a/main/cloudfoundry_client/main/__init__.py b/cloudfoundry_client/networking/v1/__init__.py similarity index 100% rename from main/cloudfoundry_client/main/__init__.py rename to cloudfoundry_client/networking/v1/__init__.py diff --git a/main/cloudfoundry_client/operations/__init__.py b/cloudfoundry_client/networking/v1/external/__init__.py similarity index 100% rename from main/cloudfoundry_client/operations/__init__.py rename to cloudfoundry_client/networking/v1/external/__init__.py diff --git a/cloudfoundry_client/networking/v1/external/policies.py b/cloudfoundry_client/networking/v1/external/policies.py new file mode 100644 index 0000000..e6afd1f --- /dev/null +++ b/cloudfoundry_client/networking/v1/external/policies.py @@ -0,0 +1,79 @@ +import logging +from cloudfoundry_client.networking.entities import EntityManager + +_logger = logging.getLogger(__name__) + + +class Policy: + def __init__(self, src_id: str, dst_id: str, proto: str, start_port: int, end_port: int): + self.source = {"id": src_id} + + self.destination = {"id": dst_id, "ports": {}} + + __protos = ["tcp", "udp"] + if proto.lower() in __protos: + self.destination["protocol"] = proto.lower() + else: + raise ValueError("unknown protocol {got}, known values are {known}" "".format(got=proto, known=__protos)) + + if 1 <= start_port <= 65535: + self.destination["ports"]["start"] = start_port + else: + raise ValueError("start port is out of range") + + if 1 <= end_port <= 65535: + self.destination["ports"]["end"] = end_port + else: + raise ValueError("end port is out of range") + + @classmethod + def from_dict(cls, policy: dict): + return cls( + src_id=policy["source"]["id"], + dst_id=policy["destination"]["id"], + proto=policy["destination"]["protocol"], + start_port=policy["destination"]["ports"]["start"], + end_port=policy["destination"]["ports"]["end"], + ) + + def dump(self): + return self.__dict__ + + +class PolicyManager(EntityManager): + def __init__(self, target_endpoint, client): + super().__init__(target_endpoint, client, "/networking/v1/external/policies") + + def create(self, policies: list[Policy]): + """create a new network policy + + Responses: + * 200 (successful) + * 400 (invalid request) + * 406 (unsupported API version) + + :param policies: the policies to create, a list of Policy objects + """ + data = list() + for policy in policies: + if not isinstance(policy, Policy): + raise TypeError + data.append(policy.dump()) + return super()._create({"policies": data}) + + def delete(self, policies: list[Policy]): + """remove a new network policy + + Responses: + * 200 (successful) + * 400 (invalid request) + * 406 (unsupported API version) + + :param policies: the policies to create, a list of Policy objects + """ + data = list() + for policy in policies: + if not isinstance(policy, Policy): + raise TypeError + data.append(policy.dump()) + return super()._delete({"policies": data}) diff --git a/main/cloudfoundry_client/operations/push/__init__.py b/cloudfoundry_client/networking/v1/external/tags.py similarity index 100% rename from main/cloudfoundry_client/operations/push/__init__.py rename to cloudfoundry_client/networking/v1/external/tags.py diff --git a/main/cloudfoundry_client/operations/push/validation/__init__.py b/cloudfoundry_client/operations/__init__.py similarity index 100% rename from main/cloudfoundry_client/operations/push/validation/__init__.py rename to cloudfoundry_client/operations/__init__.py diff --git a/main/cloudfoundry_client/v2/__init__.py b/cloudfoundry_client/operations/push/__init__.py similarity index 100% rename from main/cloudfoundry_client/v2/__init__.py rename to cloudfoundry_client/operations/push/__init__.py diff --git a/cloudfoundry_client/operations/push/cf_ignore.py b/cloudfoundry_client/operations/push/cf_ignore.py new file mode 100644 index 0000000..7436582 --- /dev/null +++ b/cloudfoundry_client/operations/push/cf_ignore.py @@ -0,0 +1,39 @@ +import fnmatch +import logging +import os + +_logger = logging.getLogger(__name__) + + +class CfIgnore(object): + def __init__(self, application_path: str): + ignore_file_path = os.path.join(application_path, ".cfignore") + self.ignore_items = [] + if os.path.isfile(ignore_file_path): + with open(ignore_file_path, "r") as ignore_file: + for line in ignore_file.readlines(): + self.ignore_items.extend(self._pattern(line.strip(" \n"))) + + def is_entry_ignored(self, relative_file: str) -> bool: + def is_relative_file_ignored(cf_ignore_entry): + _logger.debug("is_relative_file_ignored - %s - %s", cf_ignore_entry, relative_file) + file_path = ( + "/%s" % relative_file if cf_ignore_entry.startswith("/") and not relative_file.startswith("/") else relative_file + ) + return fnmatch.fnmatch(file_path, cf_ignore_entry) + + return any([is_relative_file_ignored(ignore_item) for ignore_item in self.ignore_items]) + + @staticmethod + def _pattern(pattern: str) -> list[str]: + if pattern.find("/") < 0: + return [pattern, os.path.join("**", pattern)] + elif pattern.endswith("/"): + return [ + os.path.join("/", pattern, "*"), + os.path.join("/", pattern, "**", "*"), + os.path.join("/", "**", pattern, "*"), + os.path.join("/", "**", pattern, "**", "*"), + ] + else: + return [os.path.join("/", pattern), os.path.join("/", "**", pattern)] diff --git a/main/cloudfoundry_client/operations/push/file_helper.py b/cloudfoundry_client/operations/push/file_helper.py similarity index 63% rename from main/cloudfoundry_client/operations/push/file_helper.py rename to cloudfoundry_client/operations/push/file_helper.py index ebef811..94e9b18 100644 --- a/main/cloudfoundry_client/operations/push/file_helper.py +++ b/cloudfoundry_client/operations/push/file_helper.py @@ -2,12 +2,13 @@ import os import stat import zipfile +from collections.abc import Callable, Generator class FileHelper(object): @staticmethod - def zip(file_location, directory_path, accept=None): - with zipfile.ZipFile(file_location, 'w', zipfile.ZIP_DEFLATED) as archive_out: + def zip(file_location: str, directory_path: str, accept: Callable[[str], bool] | None = None): + with zipfile.ZipFile(file_location, "w", zipfile.ZIP_DEFLATED) as archive_out: for dir_path, file_names in FileHelper.walk(directory_path): dir_full_location = os.path.join(directory_path, dir_path) if len(dir_path) > 0: @@ -15,12 +16,13 @@ def zip(file_location, directory_path, accept=None): for file_name in file_names: file_relative_location = os.path.join(dir_path, file_name) if accept is None or accept(file_relative_location): - archive_out.write(os.path.join(dir_full_location, file_name), file_relative_location, - zipfile.ZIP_DEFLATED) + archive_out.write( + os.path.join(dir_full_location, file_name), file_relative_location, zipfile.ZIP_DEFLATED + ) @staticmethod - def unzip(path, tmp_dir): - with zipfile.ZipFile(path, 'r') as zip_ref: + def unzip(path: str, tmp_dir: str): + with zipfile.ZipFile(path, "r") as zip_ref: for entry in zip_ref.namelist(): filename = os.path.basename(entry) path_to_create = os.path.join(tmp_dir, entry) @@ -30,14 +32,14 @@ def unzip(path, tmp_dir): zip_ref.extract(entry, tmp_dir) @staticmethod - def walk(path): + def walk(path: str) -> Generator[tuple[str, list[str]], None, None]: for dir_path, _, files in os.walk(path, topdown=True): - yield dir_path[len(path):].lstrip('/'), files + yield dir_path[len(path) :].lstrip("/"), files @staticmethod - def sha1(file_location): + def sha1(file_location: str) -> str: sha1 = hashlib.sha1() - with open(file_location, 'rb') as f: + with open(file_location, "rb") as f: while True: data = f.read(64 * 1024) if not data: @@ -46,10 +48,10 @@ def sha1(file_location): return sha1.hexdigest() @staticmethod - def size(path): + def size(path: str) -> int: return os.path.getsize(path) @staticmethod - def mode(file_location): + def mode(file_location: str) -> str: mode = str(oct(stat.S_IMODE(os.lstat(file_location).st_mode))) - return mode[len(mode) - 3:] + return mode[len(mode) - 3 :] diff --git a/cloudfoundry_client/operations/push/push.py b/cloudfoundry_client/operations/push/push.py new file mode 100644 index 0000000..75de4cc --- /dev/null +++ b/cloudfoundry_client/operations/push/push.py @@ -0,0 +1,368 @@ +import json +import logging +import os +import re +import shutil +import tempfile +import time + +from cloudfoundry_client.client import CloudFoundryClient +from cloudfoundry_client.operations.push.cf_ignore import CfIgnore +from cloudfoundry_client.operations.push.file_helper import FileHelper +from cloudfoundry_client.operations.push.validation.manifest import ManifestReader +from cloudfoundry_client.v2.entities import Entity + +_logger = logging.getLogger(__name__) + + +class PushOperation(object): + UPLOAD_TIMEOUT = 15 * 60 + + SPLIT_ROUTE_PATTERN = re.compile(r"(?P[a-z]+://)?(?P[^:/]+)(?P:\d+)?(?P/.*)?") + + def __init__(self, client: CloudFoundryClient): + self.client = client + + def push(self, space_id: str, manifest_path: str, restart: bool = True): + app_manifests = ManifestReader.load_application_manifests(manifest_path) + organization, space = self._retrieve_space_and_organization(space_id) + + for app_manifest in app_manifests: + if "path" in app_manifest or "docker" in app_manifest: + self._push_application(organization, space, app_manifest, restart) + + def _retrieve_space_and_organization(self, space_id: str) -> tuple[Entity, Entity]: + space = self.client.v2.spaces.get(space_id) + organization = space.organization() + return organization, space + + def _push_application(self, organization: Entity, space: Entity, app_manifest: dict, restart: bool): + app = self._init_application(space, app_manifest) + self._route_application( + organization, + space, + app, + app_manifest.get("no-route", False), + app_manifest.get("routes", []), + app_manifest.get("random-route", False), + ) + if "path" in app_manifest: + self._upload_application(app, app_manifest["path"]) + self._bind_services(space, app, app_manifest.get("services", [])) + if restart: + PushOperation._restart_application(app) + + def _init_application(self, space: Entity, app_manifest: dict) -> Entity: + app = self.client.v2.apps.get_first(name=app_manifest["name"], space_guid=space["metadata"]["guid"]) + return self._update_application(app, app_manifest) if app is not None else self._create_application(space, app_manifest) + + def _create_application(self, space: Entity, app_manifest: dict) -> Entity: + _logger.debug("Creating application %s", app_manifest["name"]) + request = self._build_request_from_manifest(app_manifest) + request["environment_json"] = PushOperation._merge_environment(None, app_manifest) + request["space_guid"] = space["metadata"]["guid"] + if request.get("health-check-type") == "http" and request.get("health-check-http-endpoint") is None: + request["health-check-http-endpoint"] = "/" + return self.client.v2.apps.create(**request) + + def _update_application(self, app: Entity, app_manifest: dict) -> Entity: + _logger.debug("Uploading application %s", app["entity"]["name"]) + request = self._build_request_from_manifest(app_manifest) + request["environment_json"] = PushOperation._merge_environment(app, app_manifest) + if ( + request.get("health-check-type") == "http" + and request.get("health-check-http-endpoint") is None + and app["entity"].get("health_check_http_endpoint") is None + ): + request["health-check-http-endpoint"] = "/" + return self.client.v2.apps.update(app["metadata"]["guid"], **request) + + def _build_request_from_manifest(self, app_manifest: dict) -> dict: + request = dict() + request.update(app_manifest) + stack = self.client.v2.stacks.get_first(name=app_manifest["stack"]) if "stack" in app_manifest else None + if stack is not None: + request["stack_guid"] = stack["metadata"]["guid"] + docker = request.pop("docker", None) + if docker is not None and "image" in docker: + request["docker_image"] = docker["image"] + request["diego"] = True + if "username" in docker and "password" in docker: + request["docker_credentials"] = dict(username=docker["username"], password=docker["password"]) + buildpacks = request.pop("buildpacks", None) + if "buildpack" not in request and buildpacks is not None and len(buildpacks) > 0: + request["buildpack"] = buildpacks[0] + return request + + @staticmethod + def _merge_environment(app: Entity | None, app_manifest: dict) -> dict: + environment = dict() + if app is not None and "environment_json" in app["entity"]: + environment.update(app["entity"]["environment_json"]) + if "env" in app_manifest: + environment.update(app_manifest["env"]) + return environment + + def _route_application( + self, organization: Entity, space: Entity, app: Entity, no_route: bool, routes: list[str], random_route: bool + ): + existing_routes = [route for route in app.routes()] + if no_route: + self._remove_all_routes(app, existing_routes) + elif len(routes) == 0 and len(existing_routes) == 0: + self._build_default_route(space, app, random_route) + else: + self._build_new_requested_routes(organization, space, app, existing_routes, routes) + + def _remove_all_routes(self, app: Entity, routes: list[Entity]): + for route in routes: + self.client.v2.apps.remove_route(app["metadata"]["guid"], route["metadata"]["guid"]) + + def _build_default_route(self, space: Entity, app: Entity, random_route: bool): + shared_domain = None + for domain in self.client.v2.shared_domains.list(): + if not domain["entity"].get("internal", False): + shared_domain = domain + break + if shared_domain is None: + raise AssertionError("No route specified and no no-route field or shared domain") + if shared_domain["entity"].get("router_group_type") == "tcp": + route = self.client.v2.routes.create_tcp_route(shared_domain["metadata"]["guid"], space["metadata"]["guid"]) + elif random_route: + route = self.client.v2.routes.create_host_route( + shared_domain["metadata"]["guid"], + space["metadata"]["guid"], + self._to_host("%s-%d" % (app["entity"]["name"], int(time.time()))), + ) + else: + route = self.client.v2.routes.create_host_route( + shared_domain["metadata"]["guid"], space["metadata"]["guid"], self._to_host(app["entity"]["name"]) + ) + self.client.v2.apps.associate_route(app["metadata"]["guid"], route["metadata"]["guid"]) + + def _build_new_requested_routes( + self, organization: Entity, space: Entity, app: Entity, existing_routes: list[Entity], requested_routes: list[str] + ): + private_domains = {domain["entity"]["name"]: domain for domain in organization.private_domains()} + shared_domains = {domain["entity"]["name"]: domain for domain in self.client.v2.shared_domains.list()} + for requested_route in requested_routes: + route, port, path = PushOperation._split_route(requested_route) + if len(path) > 0 and port is not None: + _logger.error("Neither path nor port provided for route", requested_route) + raise AssertionError("Cannot set both port and path for route: %s" % requested_route) + host, domain_name, domain = PushOperation._resolve_domain(route, private_domains, shared_domains) + route_to_map = None + if port is not None: + if domain["entity"].get("router_group_type") != "tcp": + _logger.error("Port provided in route %s for non tcp domain %s", requested_route, domain_name) + raise AssertionError("Cannot set port on route(%s) for non tcp domain" % requested_route) + elif len(host) > 0: + _logger.error("Host provided in route %s for tcp domain %s", requested_route, domain_name) + raise AssertionError( + "For route (%s) refers to domain %s that is a tcp one. " + "It is hence routed by port and not by host" + % (requested_route, domain_name) + ) + if not any( + [route["entity"]["domain_guid"] == domain["metadata"]["guid"] and route["entity"]["port"] == port] + for route in existing_routes + ): + route_to_map = self._resolve_new_tcp_route(space, domain, port) + else: + if not any( + [route["entity"]["domain_guid"] == domain["metadata"]["guid"] and route["entity"]["host"] == host] + for route in existing_routes + ): + route_to_map = self._resolve_new_host_route(space, domain, host, path) + if route_to_map is not None: + _logger.debug("Associating route %s to application %s", requested_route, app["entity"]["name"]) + self.client.v2.apps.associate_route(app["metadata"]["guid"], route_to_map["metadata"]["guid"]) + + def _resolve_new_host_route(self, space: Entity, domain: Entity, host: str, path: str) -> Entity: + existing_route = self.client.v2.routes.get_first(domain_guid=domain["metadata"]["guid"], host=host, path=path) + if existing_route is None: + _logger.debug("Creating host route %s on domain %s and path %s", host, domain["entity"]["name"], path) + existing_route = self.client.v2.routes.create_host_route( + domain["metadata"]["guid"], space["metadata"]["guid"], host, path + ) + else: + _logger.debug( + "Host route %s on domain %s and path %s already exists with guid %s", + host, + domain["entity"]["name"], + path, + existing_route["metadata"]["guid"], + ) + return existing_route + + def _resolve_new_tcp_route(self, space: Entity, domain: Entity, port: int) -> Entity: + existing_route = self.client.v2.routes.get_first(domain_guid=domain["metadata"]["guid"], port=port) + if existing_route is None: + _logger.debug("Creating tcp route %d on domain %s", port, domain["entity"]["name"]) + existing_route = self.client.v2.routes.create_tcp_route(domain["metadata"]["guid"], space["metadata"]["guid"], port) + else: + _logger.debug( + "TCP route %d on domain %s already exists with guid %s", + port, + domain["entity"]["name"], + existing_route["metadata"]["guid"], + ) + return existing_route + + @staticmethod + def _split_route(requested_route: dict[str, str]) -> tuple[str, int, str]: + route_splitted = PushOperation.SPLIT_ROUTE_PATTERN.match(requested_route["route"]) + if route_splitted is None: + raise AssertionError("Invalid route: %s" % requested_route["route"]) + domain = route_splitted.group("domain") + port = route_splitted.group("port") + path = route_splitted.group("path") + return domain, int(port[1:]) if port is not None else None, "" if path is None or path == "/" else path + + @staticmethod + def _resolve_domain( + route: str, private_domains: dict[str, Entity], shared_domains: dict[str, Entity] + ) -> tuple[str, str, Entity]: + for domains in [private_domains, shared_domains]: + if route in domains: + return "", route, domains[route] + else: + idx = route.find(".") + if 0 < idx < (len(route) - 2): + host = route[:idx] + domain = route[idx + 1 :] + if domain in domains: + return host, domain, domains[domain] + raise AssertionError("Cannot find domain for route %s" % route) + + def _upload_application(self, app: Entity, application_path: str) -> Entity: + _logger.debug("Uploading application %s", app["entity"]["name"]) + if os.path.isfile(application_path): + self._upload_application_zip(app, application_path) + elif os.path.isdir(application_path): + self._upload_application_directory(app, application_path) + else: + raise AssertionError("Path %s is neither a directory nor a file" % application_path) + + def _upload_application_zip(self, app: Entity, path: str): + _logger.debug("Unzipping file %s", path) + tmp_dir = tempfile.mkdtemp() + try: + FileHelper.unzip(path, tmp_dir) + self._upload_application_directory(app, tmp_dir) + finally: + shutil.rmtree(tmp_dir) + + def _upload_application_directory(self, app: Entity, application_path: str): + _logger.debug("Uploading application from directory %s", application_path) + _, temp_file = tempfile.mkstemp() + try: + resource_descriptions_by_path = PushOperation._load_all_resources(application_path) + + def generate_key(item: dict): + return "%s-%d" % (item["sha1"], item["size"]) + + already_uploaded_entries = [ + generate_key(item) + for item in self.client.v2.resources.match( + [dict(sha1=item["sha1"], size=item["size"]) for item in resource_descriptions_by_path.values()] + ) + ] + _logger.debug("Already uploaded %d / %d items", len(already_uploaded_entries), len(resource_descriptions_by_path)) + + FileHelper.zip( + temp_file, + application_path, + lambda item: item in resource_descriptions_by_path + and generate_key(resource_descriptions_by_path[item]) not in already_uploaded_entries, + ) + _logger.debug("Diff zip file built: %s", temp_file) + resources = [ + dict( + fn=resource_path, + sha1=resource_description["sha1"], + size=resource_description["size"], + mode=resource_description["mode"], + ) + for resource_path, resource_description in resource_descriptions_by_path.items() + if generate_key(resource_description) in already_uploaded_entries + ] + _logger.debug("Uploading bits of application") + job = self.client.v2.apps.upload(app["metadata"]["guid"], resources, temp_file, True) + self._poll_job(job) + finally: + _logger.debug("Skipping remove of zip file") + + @staticmethod + def _load_all_resources(top_directory: str) -> dict: + application_items = {} + cf_ignore = CfIgnore(top_directory) + for directory, file_names in FileHelper.walk(top_directory): + for file_name in file_names: + relative_file_location = os.path.join(directory, file_name) + if not cf_ignore.is_entry_ignored(relative_file_location): + absolute_file_location = os.path.join(top_directory, relative_file_location) + application_items[relative_file_location] = dict( + sha1=FileHelper.sha1(absolute_file_location), + size=FileHelper.size(absolute_file_location), + mode=FileHelper.mode(absolute_file_location), + ) + return application_items + + def _bind_services(self, space: Entity, app: Entity, services: list[str]): + service_instances = [ + service_instance for service_instance in space.service_instances(return_user_provided_service_instances="true") + ] + service_name_to_instance_guid = { + service_instance["entity"]["name"]: service_instance["metadata"]["guid"] for service_instance in service_instances + } + existing_service_instance_guid = [ + service_binding["entity"]["service_instance_guid"] for service_binding in app.service_bindings() + ] + for service_name in services: + service_instance_guid = service_name_to_instance_guid.get(service_name) + if service_instance_guid is None: + raise AssertionError("No service found with name %s" % service_name) + elif service_instance_guid in existing_service_instance_guid: + _logger.debug("%s already bound to %s", app["entity"]["name"], service_name) + else: + _logger.debug("Binding %s to %s", app["entity"]["name"], service_name) + self.client.v2.service_bindings.create(app["metadata"]["guid"], service_instance_guid) + + def _poll_job(self, job: Entity): + def job_not_ended(j): + return j["entity"]["status"] in ["queued", "running"] + + job_guid = job["metadata"]["guid"] + _logger.debug("Waiting for upload of application to be complete. Polling job %s...", job_guid) + started_time = time.time() + elapsed_time = 0 + + while job_not_ended(job) and elapsed_time < PushOperation.UPLOAD_TIMEOUT: + _logger.debug("Getting job status %s..", job_guid) + job = self.client.v2.jobs.get(job_guid) + if job_not_ended(job): + time.sleep(5) + elapsed_time = int(time.time() - started_time) + if job_not_ended(job): + raise AssertionError("Exceeded timeout while polling job of upload") + elif job["entity"]["status"] == "failed": + raise AssertionError("Job of upload exceeded in error: %s", json.dumps(job["entity"]["error_details"])) + else: + _logger.debug("Job ended with status %s", job["entity"]["status"]) + + @staticmethod + def _restart_application(app: Entity): + _logger.debug("Restarting application") + app.stop() + app.start() + + @staticmethod + def _to_host(host: str) -> str: + def no_space(h: str) -> str: + return re.sub(r"[\s_]+", "-", h) + + def only_alphabetical_and_hyphen(h: str) -> str: + return re.sub("[^a-z0-9-]", "", h) + + return only_alphabetical_and_hyphen(no_space(host)) diff --git a/main/cloudfoundry_client/v3/__init__.py b/cloudfoundry_client/operations/push/validation/__init__.py similarity index 100% rename from main/cloudfoundry_client/v3/__init__.py rename to cloudfoundry_client/operations/push/validation/__init__.py diff --git a/cloudfoundry_client/operations/push/validation/manifest.py b/cloudfoundry_client/operations/push/validation/manifest.py new file mode 100644 index 0000000..6272e36 --- /dev/null +++ b/cloudfoundry_client/operations/push/validation/manifest.py @@ -0,0 +1,138 @@ +import json +import os +import re + +import yaml + + +class ManifestReader(object): + SIZE_FIELD_PATTERNS = re.compile(r"^(\d+)([MG])B?$") + + POSITIVE_FIELDS = ["instances", "timeout"] + + BOOLEAN_FIELDS = ["no-route", "random-route"] + + @staticmethod + def load_application_manifests(manifest_path: str): + with open(manifest_path, "r") as fp: + manifest = yaml.safe_load(fp) + if manifest is None: + raise AssertionError("No valid yaml document found") + ManifestReader._validate_manifest(os.path.dirname(manifest_path), manifest) + return manifest["applications"] + + @staticmethod + def _validate_manifest(manifest_directory: str, manifest: dict): + for app_manifest in manifest["applications"]: + ManifestReader._validate_application_manifest(manifest_directory, app_manifest) + + @staticmethod + def _validate_application_manifest(manifest_directory: str, app_manifest: dict): + name = app_manifest.get("name") + if name is None or len(name) == 0: + raise AssertionError("name must be set") + docker_manifest = app_manifest.get("docker") + if docker_manifest is not None: + if app_manifest.get("path") is not None: + raise AssertionError("Both path and docker cannot be set") + ManifestReader._validate_application_docker(docker_manifest) + elif "path" not in app_manifest: + raise AssertionError("One of path or docker must be set") + else: + ManifestReader._absolute_path(manifest_directory, app_manifest) + ManifestReader._convert_size_fields(app_manifest) + for field in ManifestReader.POSITIVE_FIELDS: + ManifestReader._convert_positive(app_manifest, field) + for field in ManifestReader.BOOLEAN_FIELDS: + ManifestReader._convert_boolean(app_manifest, field) + ManifestReader._convert_environment(app_manifest) + ManifestReader._check_deprecated_attributes(app_manifest) + ManifestReader._validate_routes(app_manifest) + + @staticmethod + def _check_deprecated_attributes(app_manifest: dict): + if ( + app_manifest.get("hosts") is not None + or app_manifest.get("host") + or app_manifest.get("domains") is not None + or app_manifest.get("domain") + or app_manifest.get("no-hostname") is not None + ): + raise AssertionError("hosts, host, domains, domain and no-hostname are all deprecated. Use the routes attribute") + + @staticmethod + def _convert_size_fields(manifest: dict): + for field_name in ["memory", "disk_quota"]: + if field_name in manifest: + field_value = manifest[field_name].upper() + match = ManifestReader.SIZE_FIELD_PATTERNS.match(field_value) + if match is None: + raise AssertionError("Invalid %s format: %s" % (field_name, field_value)) + + size_converted = int(match.group(1)) + if match.group(2) == "M": + size_converted *= 1024 * 1024 + elif match.group(2) == "G": + size_converted *= 1024 * 1024 * 1024 + else: + raise AssertionError("Invalid %s unit: %s" % (field_name, field_value)) + manifest[field_name] = int(size_converted / (1024 * 1024)) + + @staticmethod + def _convert_positive(manifest: dict, field: str): + if field in manifest: + value = int(manifest[field]) + if value < 1: + raise AssertionError("Invalid %s value: %s. It ust be positive" % (field, value)) + manifest[field] = value + + @staticmethod + def _convert_boolean(manifest: dict, field: str): + if field in manifest: + field_value = manifest[field] + manifest[field] = field_value if isinstance(field_value, bool) else field_value.lower() == "true" + + @staticmethod + def _validate_routes(manifest: dict): + for route in manifest.get("routes", []): + if not (isinstance(route, dict)) or "route" not in route: + raise AssertionError("routes attribute must be a list of object containing a route attribute") + + @staticmethod + def _validate_application_docker(docker_manifest: dict): + docker_image = docker_manifest.get("image") + if docker_image is not None and docker_manifest.get("buildpack") is not None: + raise AssertionError("image and buildpack can not both be set for docker") + docker_username = docker_manifest.get("username") + docker_password = docker_manifest.get("password") + if docker_username is not None and docker_password is None or docker_username is None and docker_password is not None: + raise AssertionError("Docker username/password must be set together or both be unset") + if docker_username is not None and docker_password is not None and docker_image is None: + raise AssertionError("Docker image not set while docker username/password are set") + + @staticmethod + def _absolute_path(manifest_directory: str, manifest: dict): + if "path" in manifest: + path = manifest["path"] + if path == os.path.abspath(path): + manifest["path"] = path + elif manifest_directory == "" or manifest_directory == ".": + manifest["path"] = os.path.abspath(path) + else: + manifest["path"] = os.path.abspath(os.path.join(manifest_directory, path)) + + @staticmethod + def _convert_environment(app_manifest: dict): + environment = app_manifest.get("env") + if environment is not None: + if not (isinstance(environment, dict)): + raise AssertionError("'env' entry must be a dictionary") + app_manifest["env"] = { + key: json.dumps(value) for key, value in environment.items() + if value is not None and not (isinstance(value, str)) + } + app_manifest["env"].update({ + key: value for key, value in environment.items() + if value is not None and isinstance(value, str) + } + ) diff --git a/test/operations/__init__.py b/cloudfoundry_client/rlpgateway/__init__.py similarity index 100% rename from test/operations/__init__.py rename to cloudfoundry_client/rlpgateway/__init__.py diff --git a/cloudfoundry_client/rlpgateway/client.py b/cloudfoundry_client/rlpgateway/client.py new file mode 100644 index 0000000..f71d199 --- /dev/null +++ b/cloudfoundry_client/rlpgateway/client.py @@ -0,0 +1,54 @@ +import logging + +import aiohttp + +_logger = logging.getLogger(__name__) + + +class RLPGatewayClient(object): + """ + A client to read application logs directly from RLP gateway. + + The client is initialized with client id and client secret, + and provides functionality for asynchronous HTTP requests to RLP gateway endpoint. + """ + + def __init__(self, rlp_gateway_endpoint, proxy, verify_ssl, credentials_manager): + self.proxy = None + self.rlp_gateway_endpoint = rlp_gateway_endpoint + self.verify_ssl = verify_ssl + self.credentials_manager = credentials_manager + + if proxy is not None and len(proxy) > 0: + self.proxy = proxy + + async def stream_logs(self, app_guid, **kwargs): + url = f"{self.rlp_gateway_endpoint}/v2/read" + headers = { + "Authorization": self.credentials_manager._access_token, + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + } + params = {"log": "", "source_id": app_guid} + if "headers" in kwargs: + headers.update(kwargs["headers"]) + if "params" in kwargs: + params.update(kwargs["params"]) + async with aiohttp.ClientSession(headers=headers, proxy=self.proxy) as session: + async with session.get(url=url, params=params) as response: + if response.status == 204: + yield {} + else: + buffer = b"" + async for data in response.content.iter_chunked(1024): + buffer += data + if b"\n\n" in buffer and buffer.startswith(b"data:"): + log_message = buffer.split(b"\n\n")[0] + buffer = buffer.replace(log_message + b"\n\n", b"") + yield log_message + elif buffer.startswith(b"event: heartbeat") or buffer.startswith(b"event: closing"): + # Consume heartbeats to keep the connection alive + buffer = b"" + yield data + else: + yield data diff --git a/test/operations/push/__init__.py b/cloudfoundry_client/v2/__init__.py similarity index 100% rename from test/operations/push/__init__.py rename to cloudfoundry_client/v2/__init__.py diff --git a/cloudfoundry_client/v2/apps.py b/cloudfoundry_client/v2/apps.py new file mode 100644 index 0000000..26b36c6 --- /dev/null +++ b/cloudfoundry_client/v2/apps.py @@ -0,0 +1,210 @@ +import json +import logging +import os +from http import HTTPStatus +from time import sleep +from typing import TYPE_CHECKING + +from cloudfoundry_client.doppler.client import EnvelopeStream +from cloudfoundry_client.errors import InvalidStatusCode +from cloudfoundry_client.common_objects import JsonObject, Pagination +from cloudfoundry_client.v2.entities import Entity, EntityManager + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + +_logger = logging.getLogger(__name__) + + +class Application(Entity): + def instances(self) -> dict[str, JsonObject]: + return self.client.v2.apps.get_instances(self["metadata"]["guid"]) + + def start(self) -> "Application": + return self.client.v2.apps.start(self["metadata"]["guid"]) + + def stop(self) -> "Application": + return self.client.v2.apps.stop(self["metadata"]["guid"]) + + def restart_instance(self, instance_id: int): + return self.client.v2.apps.restart_instance(self["metadata"]["guid"], instance_id) + + def stats(self) -> dict[str, JsonObject]: + return self.client.v2.apps.get_stats(self["metadata"]["guid"]) + + def env(self) -> dict[str, JsonObject]: + return self.client.v2.apps.get_env(self["metadata"]["guid"]) + + def summary(self) -> JsonObject: + return self.client.v2.apps.get_summary(self["metadata"]["guid"]) + + def restage(self) -> "Application": + return self.client.v2.apps.restage(self["metadata"]["guid"]) + + def recent_logs(self) -> EnvelopeStream: + return self.client.doppler.recent_logs(self["metadata"]["guid"]) + + def stream_logs(self) -> EnvelopeStream: + return self.client.doppler.stream_logs(self["metadata"]["guid"]) + + +class AppManager(EntityManager): + APPLICATION_FIELDS = [ + "name", + "memory", + "instances", + "disk_quota", + "space_guid", + "stack_guid", + "state", + "command", + "buildpack", + "health_check_http_endpoint", + "health_check_type", + "health_check_timeout", + "diego", + "enable_ssh", + "docker_image", + "docker_credentials", + "environment_json", + "production", + "console", + "debug", + "staging_failed_reason", + "staging_failed_description", + "ports", + ] + + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__( + target_endpoint, client, "/v2/apps", lambda pairs: Application(target_endpoint, client, pairs) + ) + + def get_stats(self, application_guid: str) -> dict[str, JsonObject]: + return self._get("%s/%s/stats" % (self.entity_uri, application_guid), JsonObject) + + def get_instances(self, application_guid: str) -> dict[str, JsonObject]: + return self._get("%s/%s/instances" % (self.entity_uri, application_guid), JsonObject) + + def get_env(self, application_guid: str) -> dict[str, JsonObject]: + return self._get("%s/%s/env" % (self.entity_uri, application_guid), JsonObject) + + def get_summary(self, application_guid: str) -> JsonObject: + return self._get("%s/%s/summary" % (self.entity_uri, application_guid), JsonObject) + + def associate_route(self, application_guid: str, route_guid: str) -> Application: + self._put("%s%s/%s/routes/%s" % (self.target_endpoint, self.entity_uri, application_guid, route_guid)) + + def list_routes(self, application_guid: str, **kwargs) -> Pagination[Entity]: + return self.client.v2.routes._list("%s/%s/routes" % (self.entity_uri, application_guid), **kwargs) + + def remove_route(self, application_guid: str, route_guid: str): + self._delete("%s%s/%s/routes/%s" % (self.target_endpoint, self.entity_uri, application_guid, route_guid)) + + def list_service_bindings(self, application_guid: str, **kwargs) -> Pagination[Entity]: + return self.client.v2.service_bindings._list("%s/%s/service_bindings" % (self.entity_uri, application_guid), **kwargs) + + def start( + self, + application_guid: str, + check_time: float | None = 0.5, + timeout: float | None = 300.0, + asynchronous: bool | None = False, + ) -> Application: + result = super()._update(application_guid, dict(state="STARTED")) + if asynchronous: + return result + else: + summary = self.get_summary(application_guid) + self._wait_for_instances_in_state(application_guid, summary["instances"], "RUNNING", check_time, timeout) + return result + + def stop( + self, + application_guid: str, + check_time: float | None = 0.5, + timeout: float | None = 500.0, + asynchronous: bool | None = False, + ) -> Application: + result = super()._update(application_guid, dict(state="STOPPED")) + if asynchronous: + return result + else: + self._wait_for_instances_in_state(application_guid, 0, "STOPPED", check_time, timeout) + return result + + def restart_instance(self, application_guid: str, instance_id: int): + self._delete("%s%s/%s/instances/%s" % (self.target_endpoint, self.entity_uri, application_guid, instance_id)) + + def restage(self, application_guid: str) -> Application: + return self._post("%s%s/%s/restage" % (self.target_endpoint, self.entity_uri, application_guid)) + + def create(self, **kwargs) -> Application: + if kwargs.get("name") is None or kwargs.get("space_guid") is None: + raise AssertionError("Please provide a name and a space_guid") + request = AppManager._generate_application_update_request(**kwargs) + return super()._create(request) + + def update(self, application_guid: str, **kwargs) -> Application: + request = AppManager._generate_application_update_request(**kwargs) + return super()._update(application_guid, request) + + def remove(self, application_guid: str): + super()._remove(application_guid) + + def upload(self, application_guid: str, resources, application: str, asynchronous: bool | None = False): + application_size = os.path.getsize(application) + with open(application, "rb") as binary_file: + return self.client.put( + "%s%s/%s/bits" % (self.target_endpoint, self.entity_uri, application_guid), + params={"async": "true" if asynchronous else "false"} if asynchronous else None, + data=dict(resources=json.dumps(resources)), + files=dict( + application=( + "application.zip", + binary_file, + "application/zip", + {"Content-Length": application_size, "Content-Transfer-Encoding": "binary"}, + ) + ), + ).json(object_pairs_hook=JsonObject) + + @staticmethod + def _generate_application_update_request(**kwargs) -> dict: + return {key: kwargs[key] for key in AppManager.APPLICATION_FIELDS if key in kwargs} + + def _wait_for_instances_in_state( + self, application_guid: str, number_required: int, state_expected: str, check_time: float, timeout: float + ): + all_in_expected_state = False + sum_waiting = 0 + while not all_in_expected_state: + instances = self._safe_get_instances(application_guid) + number_in_expected_state = 0 + for instance_number, instance in list(instances.items()): + if instance["state"] == state_expected: + number_in_expected_state += 1 + # this case will make this code work for both stop and start operation + all_in_expected_state = number_in_expected_state == number_required + if not all_in_expected_state: + _logger.debug( + "_wait_for_instances_in_state - %d/%d %s", number_in_expected_state, number_required, state_expected + ) + if sum_waiting > timeout: + raise AssertionError("Failed to get state %s for %d instances" % (state_expected, number_required)) + sleep(check_time) + sum_waiting += check_time + + def _safe_get_instances(self, application_guid: str) -> dict[str, JsonObject]: + try: + return self.get_instances(application_guid) + except InvalidStatusCode as ex: + if ex.status_code == HTTPStatus.BAD_REQUEST and isinstance(ex.body, dict): + code = ex.body.get("code", -1) + # 170002: staging not finished + # 220001: instances error + if code == 220001 or code == 170002: + return JsonObject() + else: + _logger.error("") + raise diff --git a/cloudfoundry_client/v2/buildpacks.py b/cloudfoundry_client/v2/buildpacks.py new file mode 100644 index 0000000..bf71e09 --- /dev/null +++ b/cloudfoundry_client/v2/buildpacks.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class BuildpackManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/buildpacks") + + def update(self, buildpack_guid: str, parameters: dict) -> Entity: + return super()._update(buildpack_guid, parameters) diff --git a/cloudfoundry_client/v2/entities.py b/cloudfoundry_client/v2/entities.py new file mode 100644 index 0000000..8cd1142 --- /dev/null +++ b/cloudfoundry_client/v2/entities.py @@ -0,0 +1,171 @@ +from collections.abc import Callable +from functools import partial, reduce +from typing import Any, TYPE_CHECKING +from urllib.parse import quote +from requests import Response + +from cloudfoundry_client.errors import InvalidEntity +from cloudfoundry_client.common_objects import JsonObject, Request, Pagination + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class Entity(JsonObject): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient", *args, **kwargs): + super().__init__(*args, **kwargs) + self.target_endpoint = target_endpoint + self.client = client + try: + if not (isinstance(self.get("entity"), dict)): + raise InvalidEntity(**self) + + for attribute, value in list(self["entity"].items()): + domain_name, suffix = attribute.rpartition("_")[::2] + if suffix == "url": + manager_name = domain_name if domain_name.endswith("s") else "%ss" % domain_name + try: + other_manager = getattr(client.v2, manager_name) + except AttributeError: + # generic manager + + other_manager = EntityManager(target_endpoint, client, "") + if domain_name.endswith("s"): + new_method = partial(other_manager._list, value) + else: + new_method = partial(other_manager._get, value) + new_method.__name__ = domain_name + setattr(self, domain_name, new_method) + except KeyError: + raise InvalidEntity(**self) + + +EntityBuilder = Callable[[list[tuple[str, Any]]], Entity] + + +class EntityManager(object): + list_query_parameters = ["page", "results-per-page", "order-direction"] + + list_multi_parameters = ["order-by"] + + timestamp_parameters = ["timestamp"] + + def __init__( + self, target_endpoint: str, client: "CloudFoundryClient", entity_uri: str, entity_builder: EntityBuilder | None = None + ): + self.target_endpoint = target_endpoint + self.entity_uri = entity_uri + self.client = client + self.entity_builder = ( + entity_builder if entity_builder is not None else lambda pairs: Entity(target_endpoint, client, pairs) + ) + + def _list(self, requested_path: str, entity_builder: EntityBuilder | None = None, **kwargs) -> Pagination[Entity]: + url_requested = self._get_url_filtered("%s%s" % (self.target_endpoint, requested_path), **kwargs) + current_builder = self._get_entity_builder(entity_builder) + response_json = self._read_response(self.client.get(url_requested), JsonObject) + return Pagination(response_json, response_json.get("total_results", 0), + self._next_page, + lambda page: page["resources"], + lambda json_object: current_builder(list(json_object.items()))) + + def _next_page(self, current_page: JsonObject) -> JsonObject | None: + next_url = current_page.get("next_url") + if next_url is None: + return None + url_requested = "%s%s" % (self.target_endpoint, next_url) + return self._read_response(self.client.get(url_requested), JsonObject) + + def _create(self, data: dict, **kwargs) -> Entity: + url = "%s%s" % (self.target_endpoint, self.entity_uri) + return self._post(url, data, **kwargs) + + def _update(self, resource_id: str, data: dict, **kwargs): + url = "%s%s/%s" % (self.target_endpoint, self.entity_uri, resource_id) + return self._put(url, data, **kwargs) + + def _remove(self, resource_id: str, **kwargs): + url = "%s%s/%s" % (self.target_endpoint, self.entity_uri, resource_id) + self._delete(url, **kwargs) + + def _get(self, requested_path: str, entity_builder: EntityBuilder | None = None) -> Entity: + url = "%s%s" % (self.target_endpoint, requested_path) + response = self.client.get(url) + return self._read_response(response, entity_builder) + + def _post(self, url: str, data: dict | None = None, **kwargs): + response = self.client.post(url, json=data, **kwargs) + return self._read_response(response) + + def _put(self, url: str, data: dict | None = None, **kwargs): + response = self.client.put(url, json=data, **kwargs) + return self._read_response(response) + + def _delete(self, url: str, **kwargs): + self.client.delete(url, **kwargs) + + def __iter__(self) -> Pagination[Entity]: + return self.list() + + def __getitem__(self, entity_guid) -> Entity: + return self.get(entity_guid) + + def list(self, **kwargs) -> Pagination[Entity]: + return self._list(self.entity_uri, **kwargs) + + def get_first(self, **kwargs) -> Entity | None: + kwargs.setdefault("results-per-page", 1) + for entity in self._list(self.entity_uri, **kwargs): + return entity + return None + + def get(self, entity_id: str, *extra_paths) -> Entity: + if len(extra_paths) == 0: + requested_path = "%s/%s" % (self.entity_uri, entity_id) + else: + requested_path = "%s/%s/%s" % (self.entity_uri, entity_id, "/".join(extra_paths)) + return self._get(requested_path) + + def _read_response(self, response: Response, other_entity_builder: EntityBuilder | None = None): + entity_builder = self._get_entity_builder(other_entity_builder) + result = response.json(object_pairs_hook=JsonObject) + return entity_builder(list(result.items())) + + @staticmethod + def _request(**mandatory_parameters) -> Request: + return Request(**mandatory_parameters) + + def _get_entity_builder(self, entity_builder: EntityBuilder | None) -> EntityBuilder: + if entity_builder is None: + return self.entity_builder + else: + return entity_builder + + def _get_url_filtered(self, 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 parameter_name in self.list_query_parameters: + parameters.append("%s=%s" % (parameter_name, str(parameter_value))) + elif parameter_name in self.list_multi_parameters: + value_list = parameter_value + if not isinstance(value_list, (list, tuple)): + value_list = [value_list] + for value in value_list: + parameters.append("%s=%s" % (parameter_name, str(value))) + elif parameter_name in self.timestamp_parameters: + if isinstance(args[1], dict): + operator_list = args[1].keys() + for operator in operator_list: + parameters.append("q=%s" % quote("%s%s%s" % (parameter_name, operator, args[1][operator]))) + else: + parameters.append("q=%s" % quote("%s:%s" % (parameter_name, str(parameter_value)))) + elif isinstance(parameter_value, (list, tuple)): + parameters.append("q=%s" % quote("%s IN %s" % (parameter_name, ",".join(parameter_value)))) + else: + parameters.append("q=%s" % quote("%s:%s" % (parameter_name, str(parameter_value)))) + return parameters + + if len(kwargs) > 0: + return "%s?%s" % (url, "&".join(reduce(_append_encoded_parameter, sorted(list(kwargs.items())), []))) + else: + return url diff --git a/cloudfoundry_client/v2/events.py b/cloudfoundry_client/v2/events.py new file mode 100644 index 0000000..9edd455 --- /dev/null +++ b/cloudfoundry_client/v2/events.py @@ -0,0 +1,15 @@ +from collections.abc import Generator +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class EventManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/events") + + def list_by_type(self, event_type: str) -> Generator[Entity, None, None]: + return self._list(self.entity_uri, type=event_type) diff --git a/cloudfoundry_client/v2/jobs.py b/cloudfoundry_client/v2/jobs.py new file mode 100644 index 0000000..68bdaed --- /dev/null +++ b/cloudfoundry_client/v2/jobs.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import JsonObject + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class JobManager(object): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + self.target_endpoint = target_endpoint + self.client = client + + def get(self, job_guid: str) -> JsonObject: + return self.client.get("%s/v2/jobs/%s" % (self.target_endpoint, job_guid)).json(object_pairs_hook=JsonObject) diff --git a/cloudfoundry_client/v2/resources.py b/cloudfoundry_client/v2/resources.py new file mode 100644 index 0000000..a7fee10 --- /dev/null +++ b/cloudfoundry_client/v2/resources.py @@ -0,0 +1,16 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import JsonObject + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ResourceManager(object): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + self.target_endpoint = target_endpoint + self.client = client + + def match(self, items: list[dict]) -> list[JsonObject]: + response = self.client.put("%s/v2/resource_match" % self.client.info.api_endpoint, json=items) + return response.json(object_pairs_hook=JsonObject) diff --git a/cloudfoundry_client/v2/routes.py b/cloudfoundry_client/v2/routes.py new file mode 100644 index 0000000..180d52f --- /dev/null +++ b/cloudfoundry_client/v2/routes.py @@ -0,0 +1,23 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class RouteManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/routes") + + def create_tcp_route(self, domain_guid: str, space_guid: str, port: int | None = None) -> Entity: + request = self._request(domain_guid=domain_guid, space_guid=space_guid) + if port is None: + return super()._create(request, params=dict(generate_port=True)) + else: + request["port"] = port + return super()._create(request) + + def create_host_route(self, domain_guid: str, space_guid: str, host: str, path: str | None = "") -> Entity: + request = dict(domain_guid=domain_guid, space_guid=space_guid, host=host, path=path) + return super()._create(request) diff --git a/cloudfoundry_client/v2/service_bindings.py b/cloudfoundry_client/v2/service_bindings.py new file mode 100644 index 0000000..a1cb45f --- /dev/null +++ b/cloudfoundry_client/v2/service_bindings.py @@ -0,0 +1,20 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceBindingManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/service_bindings") + + def create(self, app_guid: str, instance_guid: str, parameters: dict | None = None, name: str | None = None) -> Entity: + request = self._request(app_guid=app_guid, service_instance_guid=instance_guid) + request["parameters"] = parameters + request["name"] = name + return super()._create(request) + + def remove(self, binding_id: str): + super()._remove(binding_id) diff --git a/cloudfoundry_client/v2/service_brokers.py b/cloudfoundry_client/v2/service_brokers.py new file mode 100644 index 0000000..512f807 --- /dev/null +++ b/cloudfoundry_client/v2/service_brokers.py @@ -0,0 +1,36 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceBrokerManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/service_brokers") + + def create( + self, broker_url: str, broker_name: str, auth_username: str, auth_password: str, space_guid: str | None = None + ) -> Entity: + request = self._request(broker_url=broker_url, name=broker_name, auth_username=auth_username, auth_password=auth_password) + request["space_guid"] = space_guid + return super()._create(request) + + def update( + self, + broker_guid: str, + broker_url: str | None = None, + broker_name: str | None = None, + auth_username: str | None = None, + auth_password: str | None = None, + ) -> Entity: + request = self._request() + request["broker_url"] = broker_url + request["name"] = broker_name + request["auth_username"] = auth_username + request["auth_password"] = auth_password + return super()._update(broker_guid, request) + + def remove(self, broker_guid): + super()._remove(broker_guid) diff --git a/cloudfoundry_client/v2/service_instances.py b/cloudfoundry_client/v2/service_instances.py new file mode 100644 index 0000000..f659986 --- /dev/null +++ b/cloudfoundry_client/v2/service_instances.py @@ -0,0 +1,56 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceInstanceManager(EntityManager): + list_query_parameters = ["page", "results-per-page", "order-direction", "return_user_provided_service_instances"] + + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/service_instances") + + def create( + self, + space_guid: str, + instance_name: str, + plan_guid: str, + parameters: dict | None = None, + tags: list[str] = None, + accepts_incomplete: bool | None = False, + ) -> Entity: + request = self._request(name=instance_name, space_guid=space_guid, service_plan_guid=plan_guid) + request["parameters"] = parameters + request["tags"] = tags + params = None if not accepts_incomplete else dict(accepts_incomplete="true") + return super()._create(request, params=params) + + def update( + self, + instance_guid: str, + instance_name: str | None = None, + plan_guid: str | None = None, + parameters: dict | None = None, + tags: list[str] = None, + accepts_incomplete: bool | None = False, + ) -> Entity: + request = self._request() + request["name"] = instance_name + request["service_plan_guid"] = plan_guid + request["parameters"] = parameters + request["tags"] = tags + params = None if not accepts_incomplete else dict(accepts_incomplete="true") + return super()._update(instance_guid, request, params=params) + + def list_permissions(self, instance_guid: str) -> dict[str, bool]: + return super()._get("%s/%s/permissions" % (self.entity_uri, instance_guid), dict) + + def remove(self, instance_guid: str, accepts_incomplete: bool | None = False, purge: bool | None = False): + parameters = {} + if accepts_incomplete: + parameters["accepts_incomplete"] = "true" + if purge: + parameters["purge"] = "true" + super()._remove(instance_guid, params=parameters) diff --git a/cloudfoundry_client/v2/service_keys.py b/cloudfoundry_client/v2/service_keys.py new file mode 100644 index 0000000..314b3e8 --- /dev/null +++ b/cloudfoundry_client/v2/service_keys.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceKeyManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/service_keys") + + def create(self, service_instance_guid: str, name: str, parameters: dict | None = None) -> Entity: + request = self._request(service_instance_guid=service_instance_guid, name=name) + request["parameters"] = parameters + return super()._create(request) + + def remove(self, key_guid: str): + super()._remove(key_guid) diff --git a/cloudfoundry_client/v2/service_plan_visibilities.py b/cloudfoundry_client/v2/service_plan_visibilities.py new file mode 100644 index 0000000..f882495 --- /dev/null +++ b/cloudfoundry_client/v2/service_plan_visibilities.py @@ -0,0 +1,26 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServicePlanVisibilityManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/service_plan_visibilities") + + def create(self, service_plan_guid: str, organization_guid: str) -> Entity: + request = self._request() + request["service_plan_guid"] = service_plan_guid + request["organization_guid"] = organization_guid + return super()._create(request) + + def update(self, spv_guid: str, service_plan_guid: str, organization_guid: str) -> Entity: + request = self._request() + request["service_plan_guid"] = service_plan_guid + request["organization_guid"] = organization_guid + return super()._update(spv_guid, request) + + def remove(self, spv_guid: str): + super()._remove(spv_guid) diff --git a/cloudfoundry_client/v2/service_plans.py b/cloudfoundry_client/v2/service_plans.py new file mode 100644 index 0000000..48c54f4 --- /dev/null +++ b/cloudfoundry_client/v2/service_plans.py @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import Pagination +from cloudfoundry_client.v2.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServicePlanManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/service_plans") + + def create_from_resource_file(self, path: str) -> Entity: + raise NotImplementedError("No creation allowed") + + def list_instances(self, service_plan_guid: str, **kwargs) -> Pagination[Entity]: + return self.client.v2.service_instances._list("%s/%s/service_instances" % (self.entity_uri, service_plan_guid), **kwargs) diff --git a/cloudfoundry_client/v2/spaces.py b/cloudfoundry_client/v2/spaces.py new file mode 100644 index 0000000..24f0745 --- /dev/null +++ b/cloudfoundry_client/v2/spaces.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v2.entities import EntityManager + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class SpaceManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v2/spaces") + + def delete_unmapped_routes(self, space_guid: str): + url = "%s%s/%s/unmapped_routes" % (self.target_endpoint, self.entity_uri, space_guid) + super()._delete(url) diff --git a/test/operations/push/validation/__init__.py b/cloudfoundry_client/v3/__init__.py similarity index 100% rename from test/operations/push/validation/__init__.py rename to cloudfoundry_client/v3/__init__.py diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py new file mode 100644 index 0000000..7054434 --- /dev/null +++ b/cloudfoundry_client/v3/apps.py @@ -0,0 +1,55 @@ +from typing import TYPE_CHECKING +import sys + +if sys.version_info < (3, 13): + from typing_extensions import deprecated +else: + from warnings import deprecated + +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 + + +class AppManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/apps") + + def restart(self, application_guid: str): + return super()._post("%s%s/%s/actions/restart" % (self.target_endpoint, self.entity_uri, application_guid)) + + def remove(self, application_guid: str, asynchronous: bool = True) -> str | None: + return super()._remove(application_guid, asynchronous) + + def get_env(self, application_guid: str) -> JsonObject: + return super()._get("%s%s/%s/env" % (self.target_endpoint, self.entity_uri, application_guid)) + + @deprecated("use list_routes instead") + def get_routes(self, application_guid: str) -> JsonObject: + return super()._get( + "%s%s/%s/routes" % (self.target_endpoint, self.entity_uri, application_guid)) + + def list_routes(self, application_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/routes" % (self.entity_uri, application_guid) + return super()._list(requested_path=uri, **kwargs) + + def list_droplets(self, application_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/droplets" % (self.entity_uri, application_guid) + return super()._list(requested_path=uri, **kwargs) + + 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()._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()._list(requested_path=uri, **kwargs) + + def list_deployed_revisions(self, application_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/revisions/deployed" % (self.entity_uri, application_guid) + return super()._list(requested_path=uri, **kwargs) diff --git a/cloudfoundry_client/v3/audit_events.py b/cloudfoundry_client/v3/audit_events.py new file mode 100644 index 0000000..aef9758 --- /dev/null +++ b/cloudfoundry_client/v3/audit_events.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class AuditEventManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/audit_events") diff --git a/cloudfoundry_client/v3/buildpacks.py b/cloudfoundry_client/v3/buildpacks.py new file mode 100644 index 0000000..37c71fc --- /dev/null +++ b/cloudfoundry_client/v3/buildpacks.py @@ -0,0 +1,65 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class BuildpackManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/buildpacks") + + def create( + self, + name: str, + position: int | None = 0, + enabled: bool | None = True, + locked: bool | None = False, + stack: str | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = { + "name": name, + "position": position, + "enabled": enabled, + "locked": locked, + "stack": stack, + } + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def remove(self, buildpack_guid: str, asynchronous: bool = True) -> str | None: + return super()._remove(buildpack_guid, asynchronous) + + def update( + self, + buildpack_guid: str, + name: str, + position: int | None = 0, + enabled: bool | None = True, + locked: bool | None = False, + stack: str | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = { + "name": name, + "position": position, + "enabled": enabled, + "locked": locked, + "stack": stack, + } + self._metadata(data, meta_labels, meta_annotations) + return super()._update(buildpack_guid, data) + + def upload(self, buildpack_guid: str, buildpack_zip: str, asynchronous: bool = False) -> Entity: + buildpack = super()._upload_bits(buildpack_guid, buildpack_zip) + if not asynchronous: + self.client.v3.jobs.wait_for_job_completion(buildpack.job()["guid"]) + buildpack_after_job = super().get(buildpack["guid"]) + buildpack_after_job["links"]["job"] = buildpack["links"]["job"] + buildpack_after_job.job = buildpack.job + return buildpack_after_job + return buildpack diff --git a/cloudfoundry_client/v3/domains.py b/cloudfoundry_client/v3/domains.py new file mode 100644 index 0000000..23fcba0 --- /dev/null +++ b/cloudfoundry_client/v3/domains.py @@ -0,0 +1,69 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import Pagination +from cloudfoundry_client.v3.entities import EntityManager, ToOneRelationship, ToManyRelationship, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class Domain(Entity): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient", **kwargs): + super().__init__(target_endpoint, client, **kwargs) + relationships = self["relationships"] + if "organization" in relationships: + self["relationships"]["organization"] = ToOneRelationship.from_json_object(relationships["organization"]) + if "shared_organizations" in relationships: + self["relationships"]["shared_organizations"] = ToManyRelationship.from_json_object( + relationships["shared_organizations"] + ) + + +class DomainManager(EntityManager[Domain]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/domains", Domain) + + def create( + self, + name: str, + internal: bool | None = False, + organization: ToOneRelationship | None = None, + shared_organizations: ToManyRelationship | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Domain: + data = { + "name": name, + "internal": internal, + "relationships": { + "organization": organization, + "shared_organizations": shared_organizations, + }, + } + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def list_domains_for_org(self, org_guid: str, **kwargs) -> Pagination[Entity]: + uri = "/v3/organizations/{guid}/domains".format(guid=org_guid) + return self._list(uri, **kwargs) + + def update(self, domain_guid: str, meta_labels: dict | None = None, meta_annotations: dict | None = None) -> Domain: + data = {"metadata": {"labels": meta_labels, "annotations": meta_annotations}} + return super()._update(domain_guid, data) + + def remove(self, domain_guid: str, asynchronous: bool = True) -> str | None: + return super()._remove(domain_guid, asynchronous) + + def __create_shared_domain_url(self, domain_guid: str) -> str: + # TODO use url parser for this + return "{endpoint}{entity}/{domain}/relationships/shared_organizations" "".format( + endpoint=self.target_endpoint, entity=self.entity_uri, domain=domain_guid + ) + + def share_domain(self, domain_guid: str, organization_guids: ToManyRelationship) -> ToManyRelationship: + url = self.__create_shared_domain_url(domain_guid) + return ToManyRelationship.from_json_object(super()._post(url, data=organization_guids)) + + def unshare_domain(self, domain_guid: str, org_guid: str): + url = "{uri}/{org}".format(uri=self.__create_shared_domain_url(domain_guid), org=org_guid) + super()._delete(url) diff --git a/cloudfoundry_client/v3/droplets.py b/cloudfoundry_client/v3/droplets.py new file mode 100644 index 0000000..7683672 --- /dev/null +++ b/cloudfoundry_client/v3/droplets.py @@ -0,0 +1,57 @@ +from typing import TYPE_CHECKING, Any + +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class DropletManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/droplets") + + def create(self, + app_guid: str, + process_types: dict[str, str] | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = { + "relationships": { + "app": ToOneRelationship(app_guid) + }, + } + if process_types is not None: + data["process_types"] = process_types + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def copy(self, + droplet_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=droplet_guid + ) + return self._post(url, data=data) + + def update(self, + droplet_gid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = {} + self._metadata(data, meta_labels, meta_annotations) + return super()._update(droplet_gid, data) + + def remove(self, route_gid: str): + return super()._remove(route_gid) diff --git a/cloudfoundry_client/v3/entities.py b/cloudfoundry_client/v3/entities.py new file mode 100644 index 0000000..603d801 --- /dev/null +++ b/cloudfoundry_client/v3/entities.py @@ -0,0 +1,344 @@ +import functools +from collections.abc import Callable +from json import JSONDecodeError +from typing import Any, TypeVar, TYPE_CHECKING, Type, Generic +from urllib.parse import quote, urlparse + +from requests import Response + +from cloudfoundry_client.errors import InvalidEntity +from cloudfoundry_client.common_objects import JsonObject, Request, Pagination + +if TYPE_CHECKING: + 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().__init__(**kwargs) + 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 _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"] + 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, functools.partial(lambda e: e, entity)) + self.pop("_included", None) + + @staticmethod + 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) -> str | None: + if link_method == "get": + if link_name.endswith("s"): + return "_attempt_to_paginate" + else: + return "_get" + elif link_method == "post": + return "_post" + elif link_method == "put": + return "_put" + elif link_method == "delete": + return "_delete" + return None + + +class Relationship(JsonObject): + def __init__(self, guid: str | None): + super().__init__(guid=guid) + + +class ToOneRelationship(JsonObject): + @staticmethod + def from_json_object(to_one_relationship: JsonObject): + if to_one_relationship is None: + return ToOneRelationship(None) + data = to_one_relationship.pop("data", None) + result = ToOneRelationship(None if data is None else data["guid"]) + result.update(to_one_relationship) + return result + + def __init__(self, guid: str | None): + super().__init__(data=Relationship(guid)) + self.guid = guid + + +class ToManyRelationship(JsonObject): + @staticmethod + def from_json_object(to_many_relations: JsonObject): + result = ToManyRelationship(*[relation["guid"] for relation in to_many_relations.pop("data")]) + result.update(to_many_relations) + return result + + def __init__(self, *guids: str): + super().__init__(data=[Relationship(guid) for guid in guids]) + self.guids = list(guids) + + +ENTITY_TYPE = TypeVar("ENTITY_TYPE", bound=Entity, covariant=True) + + +class EntityManager(Generic[ENTITY_TYPE]): + def __init__( + self, + target_endpoint: str, + client: "CloudFoundryClient", + entity_uri: str, + entity_type: type[ENTITY_TYPE] = Entity + ): + self.target_endpoint = target_endpoint + self.entity_uri = entity_uri + self.client = client + self.entity_type = entity_type + + def _get(self, url: str, entity_type: type[ENTITY_TYPE] | None = None, **kwargs) -> ENTITY_TYPE: + url_requested = EntityManager._get_url_with_encoded_params(url, **kwargs) + response = self.client.get(url_requested) + return self._read_response(response, entity_type) + + def _post( + self, + url: str, + entity_type: type[ENTITY_TYPE] | None = None, + data: dict | None = None, + params: dict | None = None, + files: Any = None + ) -> ENTITY_TYPE: + response = self.client.post( + url if params is None else EntityManager._get_url_with_encoded_params(url, **params), + json=data, + files=files + ) + return self._read_response(response, entity_type) + + def _put(self, url: str, data: dict, entity_type: type[ENTITY_TYPE] | None = None) -> ENTITY_TYPE: + response = self.client.put(url, json=data) + return self._read_response(response, entity_type) + + def _patch(self, url: str, data: dict, entity_type: type[ENTITY_TYPE] | None = None) -> ENTITY_TYPE: + response = self.client.patch(url, json=data) + return self._read_response(response, entity_type) + + def _delete(self, url: str) -> str | None: + response = self.client.delete(url) + return self._location(response) + + @staticmethod + def _location(response): + try: + return response.headers["Location"] + except (AttributeError, KeyError): + return None + + def _remove(self, resource_id: str, asynchronous: bool = True) -> str | None: + url = "%s%s/%s" % (self.target_endpoint, self.entity_uri, resource_id) + job_location = self._delete(url) + if job_location is not None: + job_guid = self._extract_job_guid(job_location) + if not asynchronous: + self.client.v3.jobs.wait_for_job_completion(job_guid) + else: + return job_guid + return None + + @staticmethod + def _extract_job_guid(job_location): + job_url = urlparse(job_location) + job_guid = job_url.path.rsplit("/", 1)[-1] + return job_guid + + def _list(self, requested_path: str, entity_type: type[ENTITY_TYPE] | None = None, **kwargs) -> Pagination[ENTITY_TYPE]: + url_requested = EntityManager._get_url_with_encoded_params("%s%s" % (self.target_endpoint, requested_path), **kwargs) + response_json = self._read_response(self.client.get(url_requested), JsonObject) + return self._pagination(response_json, entity_type) + + def _attempt_to_paginate(self, url_requested: str, entity_type: type[ENTITY_TYPE] | None = None) \ + -> Pagination[ENTITY_TYPE] | ENTITY_TYPE: + response_json = self._read_response(self.client.get(url_requested), JsonObject) + if "resources" in response_json: + return self._pagination(response_json, entity_type) + else: + return response_json + + def _pagination(self, page: JsonObject, entity_type: type[ENTITY_TYPE] | None = None) -> Pagination[ENTITY_TYPE]: + def _entity(json_object: JsonObject) -> ENTITY_TYPE: + return self._entity(json_object, entity_type) + + return Pagination(page, + page.get("pagination", {}).get("total_results", 0), + self._next_page, + lambda p: p["resources"], + _entity) + + def _next_page(self, current_page: JsonObject) -> JsonObject | None: + pagination = current_page.get("pagination") + if ( + pagination is None + or "next" not in pagination + or pagination["next"] is None + or pagination["next"].get("href") is None + ): + return None + return self._read_response(self.client.get(current_page["pagination"]["next"]["href"]), JsonObject) + + def _create(self, data: dict) -> ENTITY_TYPE: + url = "%s%s" % (self.target_endpoint, self.entity_uri) + return self._post(url, data=data) + + def _upload_bits(self, resource_id: str, filename: str) -> ENTITY_TYPE: + url = "%s%s/%s/upload" % (self.target_endpoint, self.entity_uri, resource_id) + files = {"bits": (filename, open(filename, "rb"))} + return self._post(url, files=files) + + def _update(self, resource_id: str, data: dict) -> ENTITY_TYPE: + url = "%s%s/%s" % (self.target_endpoint, self.entity_uri, resource_id) + return self._patch(url, data) + + def __iter__(self) -> Pagination[ENTITY_TYPE]: + return self.list() + + def __getitem__(self, entity_guid) -> ENTITY_TYPE: + return self.get(entity_guid) + + def __len__(self): + return self.len() + + def len(self, **kwargs): + url_requested = EntityManager._get_url_with_encoded_params("%s%s" % (self.target_endpoint, self.entity_uri), **kwargs) + response_json = self._read_response(self.client.get(url_requested), JsonObject) + pagination = response_json.get("pagination") + if pagination is not None: + return pagination.get("total_results", 0) + else: + return 0 + + def list(self, **kwargs) -> Pagination[ENTITY_TYPE]: + return self._list(self.entity_uri, **kwargs) + + def get_first(self, **kwargs) -> ENTITY_TYPE | None: + kwargs.setdefault("per_page", 1) + for entity in self._list(self.entity_uri, **kwargs): + return entity + return None + + def get(self, entity_id: str, *extra_paths, **kwargs) -> ENTITY_TYPE: + 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, **kwargs) + + def _read_response( + self, + response: Response, + entity_type: type[ENTITY_TYPE] | type[JsonObject] | None + ) -> JsonObject | ENTITY_TYPE: + try: + result = response.json(object_pairs_hook=JsonObject) + except JSONDecodeError: + # assume that response is empty + result = {"links": {}} + + if "Location" in response.headers: + result["links"]["job"] = { + "href": response.headers["Location"], + "method": "GET", + } + + 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") or {}).get("guid") + included_resources = result["included"].get(plural(relationship_name)) + 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) + 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: type[ENTITY_TYPE] | None) -> JsonObject | ENTITY_TYPE: + 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_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)): + 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 + + if len(kwargs) > 0: + return "%s?%s" % (url, "&".join(functools.reduce(_append_encoded_parameter, sorted(list(kwargs.items())), []))) + else: + return url + + def _metadata(self, data, meta_labels, meta_annotations): + if meta_labels or meta_annotations: + metadata = dict() + if meta_labels: + metadata["labels"] = meta_labels + if meta_annotations: + metadata["annotations"] = meta_annotations + data["metadata"] = metadata diff --git a/cloudfoundry_client/v3/feature_flags.py b/cloudfoundry_client/v3/feature_flags.py new file mode 100644 index 0000000..c92abfd --- /dev/null +++ b/cloudfoundry_client/v3/feature_flags.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class FeatureFlagManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/feature_flags") + + def update(self, name: str, enabled: bool | None = True, custom_error_message: str | None = None) -> Entity: + data = {"enabled": enabled, "custom_error_message": custom_error_message} + return super()._update(name, data) diff --git a/cloudfoundry_client/v3/isolation_segments.py b/cloudfoundry_client/v3/isolation_segments.py new file mode 100644 index 0000000..158772e --- /dev/null +++ b/cloudfoundry_client/v3/isolation_segments.py @@ -0,0 +1,59 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToManyRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class IsolationSegmentManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/isolation_segments") + + def create(self, name: str, meta_labels: dict | None = None, meta_annotations: dict | None = None) -> Entity: + data = {"name": name} + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def update( + self, isolation_segment_guid: str, name: str, meta_labels: dict | None = None, meta_annotations: dict | None = None + ) -> Entity: + data = {"name": name} + self._metadata(data, meta_labels, meta_annotations) + return super()._update(isolation_segment_guid, data) + + def entitle_organizations(self, isolation_segment_guid: str, *org_guids: str) -> ToManyRelationship: + data = ToManyRelationship(*org_guids) + return ToManyRelationship.from_json_object( + super()._post( + "%s%s/%s/relationships/organizations" % (self.target_endpoint, self.entity_uri, isolation_segment_guid), data=data + ) + ) + + def list_entitled_organizations( + self, + isolation_segment_guid: str, + ) -> ToManyRelationship: + return ToManyRelationship.from_json_object( + super()._get( + "%s%s/%s/relationships/organizations" % (self.target_endpoint, self.entity_uri, isolation_segment_guid) + ) + ) + + def list_entitled_spaces( + self, + isolation_segment_guid: str, + ) -> ToManyRelationship: + return ToManyRelationship.from_json_object( + super()._get( + "%s%s/%s/relationships/spaces" % (self.target_endpoint, self.entity_uri, isolation_segment_guid) + ) + ) + + def revoke_organization(self, isolation_segment_guid: str, org_guid: str): + super()._delete( + "%s%s/%s/relationships/organizations/%s" % (self.target_endpoint, self.entity_uri, isolation_segment_guid, org_guid) + ) + + def remove(self, isolation_segment_guid: str): + super()._remove(isolation_segment_guid) diff --git a/cloudfoundry_client/v3/jobs.py b/cloudfoundry_client/v3/jobs.py new file mode 100644 index 0000000..d5a309a --- /dev/null +++ b/cloudfoundry_client/v3/jobs.py @@ -0,0 +1,38 @@ +import types +from typing import TYPE_CHECKING + +import polling2 + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class JobTimeout(Exception): + pass + + +class JobManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/jobs") + + def wait_for_job_completion( + self, + job_guid: str, + step: int = 1, + step_function: types.FunctionType = lambda step: min(step + step, 60), + poll_forever: bool = False, + timeout: int = 600, + ) -> Entity: + try: + return polling2.poll( + lambda: self.get(job_guid), + step=step, + step_function=step_function, + poll_forever=poll_forever, + timeout=timeout, + check_success=lambda job: job["state"] == "FAILED" or job["state"] == "COMPLETE", + ) + except polling2.TimeoutException as e: + raise JobTimeout(e) diff --git a/cloudfoundry_client/v3/organization_quotas.py b/cloudfoundry_client/v3/organization_quotas.py new file mode 100644 index 0000000..5078c44 --- /dev/null +++ b/cloudfoundry_client/v3/organization_quotas.py @@ -0,0 +1,94 @@ +from typing import TYPE_CHECKING +from dataclasses import dataclass, asdict + +from cloudfoundry_client.v3.entities import Entity, EntityManager, ToManyRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +@dataclass +class AppsQuota: + total_memory_in_mb: int + per_process_memory_in_mb: int + total_instances: int + per_app_tasks: int + + +@dataclass +class ServicesQuota: + paid_services_allowed: bool + total_service_instances: int + total_service_keys: int + + +@dataclass +class RoutesQuota: + total_routes: int + total_reserved_ports: int + + +@dataclass +class DomainsQuota: + total_domains: int + + +class OrganizationQuotaManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/organization_quotas") + + def create( + self, + name: str, + apps_quota: AppsQuota | None = None, + services_quota: ServicesQuota | None = None, + routes_quota: RoutesQuota | None = None, + domains_quota: DomainsQuota | None = None, + assigned_organizations: ToManyRelationship | None = None, + ) -> Entity: + data = self._asdict(name, apps_quota, services_quota, routes_quota, domains_quota, assigned_organizations) + return super()._create(data) + + def update( + self, + guid: str, + name: str, + apps_quota: AppsQuota | None = None, + services_quota: ServicesQuota | None = None, + routes_quota: RoutesQuota | None = None, + domains_quota: DomainsQuota | None = None, + ) -> Entity: + data = self._asdict(name, apps_quota, services_quota, routes_quota, domains_quota) + return super()._update(guid, data) + + def apply_to_organizations(self, guid: str, organizations: ToManyRelationship) -> ToManyRelationship: + return ToManyRelationship.from_json_object( + super()._post( + "%s%s/%s/relationships/organizations" % (self.target_endpoint, self.entity_uri, guid), data=organizations + ) + ) + + def remove(self, guid: str, asynchronous: bool = True) -> str | None: + return super()._remove(guid, asynchronous) + + def _asdict( + self, + name: str, + apps_quota: AppsQuota | None = None, + services_quota: ServicesQuota | None = None, + routes_quota: RoutesQuota | None = None, + domains_quota: DomainsQuota | None = None, + assigned_organizations: ToManyRelationship | None = None, + ): + data = {"name": name} + if apps_quota: + data["apps"] = asdict(apps_quota) + if services_quota: + data["services"] = asdict(services_quota) + if routes_quota: + data["routes"] = asdict(routes_quota) + if domains_quota: + data["domains"] = asdict(domains_quota) + if assigned_organizations: + data["relationships"] = {"organizations": assigned_organizations} + return data diff --git a/cloudfoundry_client/v3/organizations.py b/cloudfoundry_client/v3/organizations.py new file mode 100644 index 0000000..2b22cc2 --- /dev/null +++ b/cloudfoundry_client/v3/organizations.py @@ -0,0 +1,59 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import Pagination +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class OrganizationManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/organizations") + + def create( + self, name: str, suspended: bool, meta_labels: dict | None = None, meta_annotations: dict | None = None + ) -> Entity: + data = {"name": name, "suspended": suspended} + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def update( + self, + guid: str, + name: str, + suspended: bool | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {"name": name} + if suspended is not None: + data["suspended"] = suspended + self._metadata(data, meta_labels, meta_annotations) + return super()._update(guid, data) + + def remove(self, guid: str, asynchronous: bool = True) -> str | None: + return super()._remove(guid, asynchronous) + + def assign_default_isolation_segment(self, org_guid: str, iso_seg_guid: str) -> Entity: + return ToOneRelationship.from_json_object( + super()._patch( + "%s%s/%s/relationships/default_isolation_segment" % (self.target_endpoint, self.entity_uri, org_guid), + data=ToOneRelationship(iso_seg_guid), + ) + ) + + def get_default_isolation_segment(self, guid: str) -> ToOneRelationship: + return ToOneRelationship.from_json_object( + super().get(guid, "relationships", "default_isolation_segment") + ) + + def list_domains(self, guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/domains" % (self.entity_uri, guid) + return super()._list(requested_path=uri, **kwargs) + + def get_default_domain(self, guid: str) -> Entity: + return super().get(guid, "domains", "default") + + def get_usage_summary(self, guid: str) -> Entity: + return super().get(guid, "usage_summary") diff --git a/cloudfoundry_client/v3/packages.py b/cloudfoundry_client/v3/packages.py new file mode 100644 index 0000000..c45ea0a --- /dev/null +++ b/cloudfoundry_client/v3/packages.py @@ -0,0 +1,55 @@ +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, 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().__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()._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()._list(requested_path=uri, **kwargs) diff --git a/cloudfoundry_client/v3/processes.py b/cloudfoundry_client/v3/processes.py new file mode 100644 index 0000000..7a5065b --- /dev/null +++ b/cloudfoundry_client/v3/processes.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ProcessManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/processes") diff --git a/cloudfoundry_client/v3/roles.py b/cloudfoundry_client/v3/roles.py new file mode 100644 index 0000000..8771a94 --- /dev/null +++ b/cloudfoundry_client/v3/roles.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class RoleManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/roles") + + def remove(self, role_guid: str, asynchronous: bool = True) -> str | None: + return super()._remove(role_guid, asynchronous) diff --git a/cloudfoundry_client/v3/routes.py b/cloudfoundry_client/v3/routes.py new file mode 100644 index 0000000..3265d5f --- /dev/null +++ b/cloudfoundry_client/v3/routes.py @@ -0,0 +1,58 @@ +from enum import Enum +from typing import TYPE_CHECKING, Any + +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class LoadBalancing(Enum): + ROUND_ROBIN = 'round-robin' + LEAST_CONNECTION = 'least-connection' + + +class RouteManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/routes") + + def create(self, + space_guid: str, + domain_guid: str, + host: str | None = None, + path: str | None = None, + port: int | None = None, + load_balancing: LoadBalancing | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = { + "relationships": { + "space": ToOneRelationship(space_guid), "domain": ToOneRelationship(domain_guid) + }, + } + if host is not None: + data["host"] = host + if port is not None: + data["port"] = port + if path is not None: + data["path"] = path + if load_balancing is not None: + data["options"] = {"loadbalancing": load_balancing.value} + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def update(self, + route_gid: str, + load_balancing: LoadBalancing | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = {} + if load_balancing is not None: + data["options"] = {"loadbalancing": load_balancing.value} + self._metadata(data, meta_labels, meta_annotations) + return super()._update(route_gid, data) + + def remove(self, route_gid: str): + return super()._remove(route_gid) diff --git a/cloudfoundry_client/v3/security_groups.py b/cloudfoundry_client/v3/security_groups.py new file mode 100644 index 0000000..77e766b --- /dev/null +++ b/cloudfoundry_client/v3/security_groups.py @@ -0,0 +1,115 @@ +from dataclasses import dataclass, asdict +from enum import Enum, auto +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, ToManyRelationship, Entity, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class RuleProtocol(Enum): + TCP = auto() + UDP = auto() + ICMP = auto() + ALL = auto() + + def __repr__(self): + return '%s' % self.name.lower() + + +@dataclass +class Rule: + protocol: RuleProtocol + destination: str + ports: str | None = None + type: int | None = None + code: int | None = None + description: str | None = None + log: bool | None = None + + +@dataclass +class GloballyEnabled: + running: bool | None = None + staging: bool | None = None + + +class SecurityGroupManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/security_groups") + + def create(self, + name: str, + rules: list[Rule] | None = None, + globally_enabled: GloballyEnabled | None = None, + staging_spaces: ToManyRelationship | None = None, + running_spaces: ToManyRelationship | None = None) -> Entity: + payload = self._generate_payload(name, rules, globally_enabled, staging_spaces, running_spaces) + return super()._create(payload) + + def update(self, + security_group_id: str, + name: str | None = None, + rules: list[Rule] | None = None, + globally_enabled: GloballyEnabled | None = None, + staging_spaces: ToManyRelationship | None = None, + running_spaces: ToManyRelationship | None = None) -> Entity: + payload = self._generate_payload(name, rules, globally_enabled, staging_spaces, running_spaces) + return super()._update(security_group_id, payload) + + def remove(self, security_group_id: str, asynchronous: bool = True) -> str | None: + return super()._remove(security_group_id, asynchronous) + + def bind_running_security_group_to_spaces(self, security_group_id: str, space_guids: ToManyRelationship) \ + -> ToManyRelationship: + relationship = "running_spaces" + return self._bind_spaces(security_group_id, space_guids, relationship) + + def bind_staging_security_group_to_spaces(self, security_group_id: str, space_guids: ToManyRelationship) \ + -> ToManyRelationship: + relationship = "staging_spaces" + return self._bind_spaces(security_group_id, space_guids, relationship) + + def unbind_running_security_group_from_space(self, security_group_id: str, space_guid: ToOneRelationship): + relationship = "running_spaces" + return self._unbind_space(security_group_id, space_guid, relationship) + + def unbind_staging_security_group_from_space(self, security_group_id: str, space_guid: ToOneRelationship): + relationship = "staging_spaces" + return self._unbind_space(security_group_id, space_guid, relationship) + + def _bind_spaces(self, security_group_id: str, space_guids: ToManyRelationship, relationship: str) \ + -> ToManyRelationship: + url = "%s%s/%s/relationships/%s" % (self.target_endpoint, self.entity_uri, security_group_id, relationship) + return ToManyRelationship.from_json_object(super()._post(url, data=space_guids)) + + def _unbind_space(self, security_group_id: str, space_guid: ToOneRelationship, relationship: str): + url = "%s%s/%s/relationships/%s/%s" \ + % (self.target_endpoint, self.entity_uri, security_group_id, relationship, space_guid.guid) + super()._delete(url) + + @staticmethod + def _generate_payload(name: str | None, + rules: list[Rule] | None, + globally_enabled: GloballyEnabled | None, + staging_spaces: ToManyRelationship | None, + running_spaces: ToManyRelationship | None): + payload = {} + if name: + payload["name"] = name + if rules: + payload["rules"] = [asdict(rule, dict_factory=lambda x: {k: repr(v) if k == "protocol" else v + for (k, v) in x if v is not None}) + for rule in rules] + if globally_enabled: + payload["globally_enabled"] = asdict(globally_enabled, + dict_factory=lambda x: {k: v for (k, v) in x if v is not None}) + relationships = dict() + if staging_spaces: + relationships["staging_spaces"] = staging_spaces + if running_spaces: + relationships["running_spaces"] = running_spaces + if len(relationships) > 0: + payload["relationships"] = relationships + return payload diff --git a/cloudfoundry_client/v3/service_brokers.py b/cloudfoundry_client/v3/service_brokers.py new file mode 100644 index 0000000..ba0b661 --- /dev/null +++ b/cloudfoundry_client/v3/service_brokers.py @@ -0,0 +1,51 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceBrokerManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/service_brokers") + + def create( + self, + name: str, + url: str, + auth_username: str, + auth_password: str, + space_guid: str | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + credentials = {"type": "basic", "credentials": {"username": auth_username, "password": auth_password}} + payload = dict(name=name, url=url, authentication=credentials) + self._metadata(payload, meta_labels, meta_annotations) + if space_guid: + payload["relationships"] = dict(space=ToOneRelationship(space_guid)) + return super()._create(payload) + + def update( + self, + guid: str, + name: str | None = None, + url: str | None = None, + auth_username: str | None = None, + auth_password: str | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + payload = dict() + if name: + payload["name"] = name + if url: + payload["url"] = url + if auth_username and auth_password: + payload["authentication"] = {"type": "basic", "credentials": {"username": auth_username, "password": auth_password}} + self._metadata(payload, meta_labels, meta_annotations) + return super()._update(guid, payload) + + def remove(self, guid: str, asynchronous: bool = True) -> str | None: + return super()._remove(guid, asynchronous) diff --git a/cloudfoundry_client/v3/service_credential_bindings.py b/cloudfoundry_client/v3/service_credential_bindings.py new file mode 100644 index 0000000..da17322 --- /dev/null +++ b/cloudfoundry_client/v3/service_credential_bindings.py @@ -0,0 +1,50 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceCredentialBindingManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/service_credential_bindings") + + def create( + self, + name: str, + service_credential_binding_type: str, + service_instance_guid: str, + application_guid: str | None, + parameters: dict | None, + meta_labels: dict | None, + meta_annotations: dict | None, + asynchronous: bool = True, + ) -> str | Entity | None: + data = { + "name": name, + "type": service_credential_binding_type, + "relationships": {"service_instance": ToOneRelationship(service_instance_guid)}, + } + if application_guid: + data["relationships"]["app"] = ToOneRelationship(application_guid) + if parameters: + data["parameters"] = parameters + if meta_labels or meta_annotations: + metadata = dict() + if meta_labels: + metadata["labels"] = meta_labels + if meta_annotations: + metadata["annotations"] = meta_annotations + data["metadata"] = metadata + url = "%s%s" % (self.target_endpoint, self.entity_uri) + response = self.client.post(url, json=data) + location = super()._location(response) + if location: + job_guid = super()._extract_job_guid(location) + if asynchronous: + return job_guid + else: + self.client.v3.jobs.wait_for_job_completion(job_guid) + return None + return super()._read_response(response, None) diff --git a/cloudfoundry_client/v3/service_instances.py b/cloudfoundry_client/v3/service_instances.py new file mode 100644 index 0000000..c445c91 --- /dev/null +++ b/cloudfoundry_client/v3/service_instances.py @@ -0,0 +1,68 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import JsonObject +from cloudfoundry_client.v3.entities import Entity, EntityManager, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceInstanceManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/service_instances") + + def create( + self, + name: str, + space_guid: str, + service_plan_guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + parameters: dict | None = None, + tags: list[str] | None = None, + ) -> Entity: + data = { + "name": name, + "type": "managed", + "relationships": {"space": ToOneRelationship(space_guid), "service_plan": ToOneRelationship(service_plan_guid)}, + } + if parameters: + data["parameters"] = parameters + if tags: + data["tags"] = tags + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def update( + self, + instance_guid: str, + name: str | None = None, + parameters: dict | None = None, + service_plan: str | None = None, + maintenance_info: str | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + tags: list[str] | None = None + ) -> Entity: + data = {} + if name: + data["name"] = name + if parameters: + data["parameters"] = parameters + if service_plan: + data["relationships"] = { + "service_plan": ToOneRelationship(service_plan)} + if maintenance_info: + data["maintenance_info"] = {"version": maintenance_info} + if tags: + data["tags"] = tags + super()._metadata(data, meta_labels, meta_annotations) + return super()._update(instance_guid, data) + + def remove(self, guid: str, asynchronous: bool = True): + super()._remove(guid, asynchronous) + + def get_permissions(self, instance_guid: str) -> JsonObject: + return super()._get( + "%s%s/%s/permissions" % (self.target_endpoint, self.entity_uri, instance_guid) + ) diff --git a/cloudfoundry_client/v3/service_offerings.py b/cloudfoundry_client/v3/service_offerings.py new file mode 100644 index 0000000..5e6d3aa --- /dev/null +++ b/cloudfoundry_client/v3/service_offerings.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServiceOfferingsManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/service_offerings") + + def update(self, guid: str, meta_labels: dict | None = None, meta_annotations: dict | None = None) -> Entity: + payload = dict() + self._metadata(payload, meta_labels, meta_annotations) + return super()._update(guid, payload) + + def remove(self, guid: str, purge: bool = False) -> None: + url = f"{self.target_endpoint}{self.entity_uri}/{guid}" + if purge: + url += "?purge=true" + super()._delete(url) diff --git a/cloudfoundry_client/v3/service_plans.py b/cloudfoundry_client/v3/service_plans.py new file mode 100644 index 0000000..fce669b --- /dev/null +++ b/cloudfoundry_client/v3/service_plans.py @@ -0,0 +1,50 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class ServicePlanManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/service_plans") + + def update( + self, + guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + payload = {"metadata": {}} + self._metadata(payload, meta_labels, meta_annotations) + return super()._update(guid, payload) + + def remove(self, guid: str): + super()._remove(guid) + + def get_visibility(self, service_plan_guid: str) -> dict: + return super()._get(f"{self.target_endpoint}{self.entity_uri}/{service_plan_guid}/visibility") + + # Updates a service plan visibility. It behaves similar to the POST service plan visibility endpoint but + # this endpoint will REPLACE the existing list of organizations when the service plan is organization visible. + def update_visibility(self, service_plan_guid: str, type: str, organizations: list[dict] | None = None) -> dict: + payload = {"type": type} + if organizations: + payload["organizations"] = organizations + return super()._patch( + url=f"{self.target_endpoint}{self.entity_uri}/{service_plan_guid}/visibility", data=payload + ) + + # Applies a service plan visibility. It behaves similar to the PATCH service plan visibility endpoint but + # this endpoint will APPEND to the existing list of organizations when the service plan is organization visible. + def apply_visibility_to_extra_orgs(self, service_plan_guid: str, organizations: list[dict]) -> dict: + payload = {"type": "organization", "organizations": organizations} + return super()._post( + url=f"{self.target_endpoint}{self.entity_uri}/{service_plan_guid}/visibility", data=payload, files=None + ) + + def remove_org_from_service_plan_visibility(self, service_plan_guid: str, org_guid: str): + super()._delete( + url=f"{self.target_endpoint}{self.entity_uri}/{service_plan_guid}/visibility/{org_guid}" + ) diff --git a/cloudfoundry_client/v3/spaces.py b/cloudfoundry_client/v3/spaces.py new file mode 100644 index 0000000..42f5a7d --- /dev/null +++ b/cloudfoundry_client/v3/spaces.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, ToOneRelationship, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class SpaceManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/spaces") + + def create(self, name: str, org_guid: str) -> Entity: + return super()._create(dict(name=name, relationships=dict(organization=ToOneRelationship(org_guid)))) + + def update(self, space_guid: str, name: str) -> Entity: + return super()._update(space_guid, dict(name=name)) + + def get_assigned_isolation_segment(self, space_guid: str) -> ToOneRelationship: + return ToOneRelationship.from_json_object( + super()._get( + "%s%s/%s/relationships/isolation_segment" % (self.target_endpoint, self.entity_uri, space_guid) + ) + ) + + def assign_isolation_segment(self, space_guid: str, isolation_segment_guid: str | None) -> ToOneRelationship: + return ToOneRelationship.from_json_object( + super()._patch( + "%s%s/%s/relationships/isolation_segment" % (self.target_endpoint, self.entity_uri, space_guid), + dict(data=None) if isolation_segment_guid is None else ToOneRelationship(isolation_segment_guid), + ) + ) + + def remove(self, space_guid: str): + super()._remove(space_guid) diff --git a/cloudfoundry_client/v3/stacks.py b/cloudfoundry_client/v3/stacks.py new file mode 100644 index 0000000..751561f --- /dev/null +++ b/cloudfoundry_client/v3/stacks.py @@ -0,0 +1,42 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import Pagination +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class StackMananager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/stacks") + + def create( + self, + name: str, + description: str | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {"name": name} + if description is not None: + data["description"] = description + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def update( + self, + stack_guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {} + self._metadata(data, meta_labels, meta_annotations) + return super()._update(stack_guid, data) + + def list_apps(self, stack_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/apps" % (self.entity_uri, stack_guid) + return super()._list(requested_path=uri, **kwargs) + + def remove(self, stack_guid: str): + super()._remove(stack_guid) diff --git a/cloudfoundry_client/v3/tasks.py b/cloudfoundry_client/v3/tasks.py new file mode 100644 index 0000000..a7a0112 --- /dev/null +++ b/cloudfoundry_client/v3/tasks.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class TaskManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/tasks") + + def create( + self, + application_guid: str, + command: str, + name: str | None = None, + disk_in_mb: int | None = None, + memory_in_mb: int | None = None, + droplet_guid: str | None = None, + ) -> Entity: + request = self._request(command=command) + request["name"] = name + request["disk_in_mb"] = disk_in_mb + request["memory_in_mb"] = memory_in_mb + request["droplet_guid"] = droplet_guid + return self._post("%s/v3/apps/%s/tasks" % (self.target_endpoint, application_guid), data=request) + + def cancel(self, task_guid: str) -> Entity: + return self._post("%s/v3/tasks/%s/actions/cancel" % (self.target_endpoint, task_guid)) diff --git a/cloudfoundry_client/v3/users.py b/cloudfoundry_client/v3/users.py new file mode 100644 index 0000000..6fad9f1 --- /dev/null +++ b/cloudfoundry_client/v3/users.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import Entity, EntityManager + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class UserManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super().__init__(target_endpoint, client, "/v3/users") + + def create( + self, + user_info: str | tuple[str, str], + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {} + if isinstance(user_info, str): + data["guid"] = user_info + else: + username, origin = user_info + data["username"] = username + data["origin"] = origin + self._metadata(data, meta_labels, meta_annotations) + return super()._create(data) + + def update( + self, + guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {} + self._metadata(data, meta_labels, meta_annotations) + return super()._update(guid, data) + + def remove(self, guid: str) -> str | None: + return super()._remove(guid) diff --git a/integration/config_test.py b/integration/config_test.py index 6022e65..b33bfde 100644 --- a/integration/config_test.py +++ b/integration/config_test.py @@ -1,7 +1,8 @@ import logging import os +from configparser import ConfigParser, NoSectionError, NoOptionError +from http.client import HTTPConnection -from imported import ConfigParser, NoSectionError, NoOptionError, HTTPConnection from cloudfoundry_client.client import CloudFoundryClient _client = None @@ -12,29 +13,28 @@ def _init_logging(): HTTPConnection.debuglevel = 1 - logging.basicConfig(level=logging.DEBUG, - format='%(levelname)5s - %(name)s - %(message)s') + logging.basicConfig(level=logging.DEBUG, format="%(levelname)5s - %(name)s - %(message)s") requests_log = logging.getLogger("requests.packages.urllib3") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True def get_resource_dir(): - result = os.path.join(os.path.dirname(__file__), 'resources') + result = os.path.join(os.path.dirname(__file__), "resources") if not (os.path.exists(result) and os.path.isdir(result)): - raise IOError('Directory %s must exist.' % result) + raise IOError("Directory %s must exist." % result) return result def get_resource(file_name): result = os.path.join(get_resource_dir(), file_name) if not (os.path.exists(result) and os.path.isfile(result) and os.access(result, os.R_OK)): - raise IOError('File %s must exist.' % result) + raise IOError("File %s must exist." % result) return result def get_build_dir(): - result = os.path.join(os.path.dirname(__file__), '..', 'dist') + result = os.path.join(os.path.dirname(__file__), "..", "dist") return result @@ -43,33 +43,31 @@ def build_client_from_configuration(): if _client is None: _init_logging() cfg = ConfigParser() - cfg.read(get_resource('test.properties')) + cfg.read(get_resource("test.properties")) proxy = None try: - http = cfg.get('proxy', 'http') - https = cfg.get('proxy', 'https') + http = cfg.get("proxy", "http") + https = cfg.get("proxy", "https") proxy = dict(http=http, https=https) except (NoSectionError, NoOptionError): pass skip_verification = False try: - skip_verification_str = cfg.get('service', 'skip_ssl_verification') - skip_verification = skip_verification_str.lower() == 'true' + skip_verification_str = cfg.get("service", "skip_ssl_verification") + skip_verification = skip_verification_str.lower() == "true" except (NoSectionError, NoOptionError): pass - client = CloudFoundryClient(cfg.get('service', 'target_endpoint'), proxy=proxy, - skip_verification=skip_verification) - client.init_with_user_credentials(cfg.get('authentification', 'login'), - cfg.get('authentification', 'password')) - client.org_guid = cfg.get('test_data', 'org_guid') - client.space_guid = cfg.get('test_data', 'space_guid') - client.app_guid = cfg.get('test_data', 'app_guid') - client.log_app_guid = cfg.get('test_data', 'log_app_guid') - client.service_guid = cfg.get('test_data', 'service_guid') - client.service_name = cfg.get('test_data', 'service_name') - client.plan_guid = cfg.get('test_data', 'plan_guid') - client.creation_parameters = eval(cfg.get('test_data', 'creation_parameters')) - client.update_parameters = eval(cfg.get('test_data', 'update_parameters')) + client = CloudFoundryClient(cfg.get("service", "target_endpoint"), proxy=proxy, skip_verification=skip_verification) + client.init_with_user_credentials(cfg.get("authentification", "login"), cfg.get("authentification", "password")) + client.org_guid = cfg.get("test_data", "org_guid") + client.space_guid = cfg.get("test_data", "space_guid") + client.app_guid = cfg.get("test_data", "app_guid") + client.log_app_guid = cfg.get("test_data", "log_app_guid") + client.service_guid = cfg.get("test_data", "service_guid") + client.service_name = cfg.get("test_data", "service_name") + client.plan_guid = cfg.get("test_data", "plan_guid") + client.creation_parameters = eval(cfg.get("test_data", "creation_parameters")) + client.update_parameters = eval(cfg.get("test_data", "update_parameters")) _client = client return _client diff --git a/integration/imported.py b/integration/imported.py deleted file mode 100644 index 3734e77..0000000 --- a/integration/imported.py +++ /dev/null @@ -1,15 +0,0 @@ -import sys - -if sys.version_info.major == 2: - from ConfigParser import ConfigParser, NoSectionError, NoOptionError - from httplib import SEE_OTHER, CREATED, NO_CONTENT,HTTPConnection - -elif sys.version_info.major == 3: - from configparser import ConfigParser, NoSectionError, NoOptionError - from http import HTTPStatus - SEE_OTHER = HTTPStatus.SEE_OTHER.value - CREATED = HTTPStatus.CREATED.value - NO_CONTENT = HTTPStatus.NO_CONTENT.value - from http.client import HTTPConnection -else: - raise ImportError('Invalid major version: %d' % sys.version_info.major) \ No newline at end of file diff --git a/integration/test_applications.py b/integration/test_applications.py deleted file mode 100644 index 203f870..0000000 --- a/integration/test_applications.py +++ /dev/null @@ -1,67 +0,0 @@ -from config_test import build_client_from_configuration -import unittest -import logging -import json - -from cloudfoundry_client.errors import InvalidStatusCode -from cloudfoundry_client.imported import NOT_FOUND, BAD_REQUEST - -_logger = logging.getLogger(__name__) - - -class TestApps(unittest.TestCase): - def test_list(self): - cpt = 0 - client = build_client_from_configuration() - for application in client.v2.apps.list(space_guid=client.space_guid): - _logger.debug('- %s' % application['entity']['name']) - if cpt == 0: - _logger.debug('- %s' % application['metadata']['guid']) - self.assertIsNotNone(client.v2.apps.get_first(space_guid=client.space_guid, - name=application['entity']['name'])) - self.assertIsNotNone(client.v2.apps.get(application['metadata']['guid'])) - try: - client.v2.apps.get('%s-0' % application['metadata']['guid']) - self.fail('Should not have been found') - except InvalidStatusCode as e: - self.assertEquals(e.status_code, NOT_FOUND) - try: - instances = client.v2.apps.get_instances(application['metadata']['guid']) - self.assertIsNotNone(instances) - self.assertEquals(len(instances), application['entity']['instances']) - _logger.debug('instances = %s', json.dumps(instances)) - except InvalidStatusCode as e: - #instance is stopped - self.assertEquals(e.status_code, BAD_REQUEST) - self.assertIsInstance(e.body, dict) - self.assertEqual(e.body.get('error_code'), 'CF-InstancesError') - try: - stats = client.v2.apps.get_stats(application['metadata']['guid']) - self.assertIsNotNone(stats) - self.assertEquals(len(stats), application['entity']['instances']) - self.assertEquals(len(stats), application['entity']['instances']) - _logger.debug('stats = %s', json.dumps(stats)) - except InvalidStatusCode as e: - # instance is stopped - self.assertEquals(e.status_code, BAD_REQUEST) - self.assertIsInstance(e.body, dict) - self.assertEqual(e.body.get('error_code'), 'CF-AppStoppedStatsError') - env = client.v2.apps.get_env(application['metadata']['guid']) - self.assertIsNotNone(env) - self.assertIsNotNone(env.get('application_env_json', None)) - self.assertIsNotNone(env['application_env_json'].get('VCAP_APPLICATION', None)) - self.assertGreater(len(env['application_env_json']['VCAP_APPLICATION'].get('application_uris', [])), 0) - _logger.debug('env = %s', json.dumps(env)) - cpt += 1 - - _logger.debug('test applications list - %d found', cpt) - - def test_start(self): - client = build_client_from_configuration() - - _logger.debug('start - %s', client.v2.apps.start(client.app_guid, async=False)) - - def test_stop(self): - client = build_client_from_configuration() - _logger.debug('stop - %s', client.v2.apps.stop(client.app_guid, async=False)) - diff --git a/integration/test_service_instances.py b/integration/test_service_instances.py deleted file mode 100644 index cac2200..0000000 --- a/integration/test_service_instances.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging -import unittest - -from config_test import build_client_from_configuration - -_logger = logging.getLogger(__name__) - - -class TestServiceInstances(unittest.TestCase): - def test_create_update_delete(self): - client = build_client_from_configuration() - result = client.v2.service_instances.create(client.space_guid, 'test_name', client.plan_guid, - client.creation_parameters) - if len(client.update_parameters) > 0: - client.v2.service_instances.update(result['metadata']['guid'], client.update_parameters) - else: - _logger.warning('update test skipped') - client.v2.service_instances.remove(result['metadata']['guid']) - - def test_get(self): - client = build_client_from_configuration() - cpt = 0 - for instance in client.v2.service_instances.list(): - if cpt == 0: - self.assertIsNotNone( - client.v2.service_instances.get_first(space_guid=instance['entity']['space_guid'])) - self.assertIsNotNone( - client.v2.service_instances.get(instance['metadata']['guid'])) - self.assertIsNotNone( - client.v2.service_instances.list_permissions(instance['metadata']['guid'])) - cpt += 1 - _logger.debug('test_get - %d found', cpt) diff --git a/integration/v2/test_applications.py b/integration/v2/test_applications.py new file mode 100644 index 0000000..33a2bec --- /dev/null +++ b/integration/v2/test_applications.py @@ -0,0 +1,66 @@ +import json +import logging +import unittest +from http import HTTPStatus + +from config_test import build_client_from_configuration + +from cloudfoundry_client.errors import InvalidStatusCode + +_logger = logging.getLogger(__name__) + + +class TestApps(unittest.TestCase): + def test_list(self): + cpt = 0 + client = build_client_from_configuration() + for application in client.v2.apps.list(space_guid=client.space_guid): + _logger.debug("- %s" % application["entity"]["name"]) + if cpt == 0: + _logger.debug("- %s" % application["metadata"]["guid"]) + self.assertIsNotNone(client.v2.apps.get_first(space_guid=client.space_guid, name=application["entity"]["name"])) + self.assertIsNotNone(client.v2.apps.get(application["metadata"]["guid"])) + try: + client.v2.apps.get("%s-0" % application["metadata"]["guid"]) + self.fail("Should not have been found") + except InvalidStatusCode as e: + self.assertEquals(e.status_code, HTTPStatus.NOT_FOUND) + try: + instances = client.v2.apps.get_instances(application["metadata"]["guid"]) + self.assertIsNotNone(instances) + self.assertEquals(len(instances), application["entity"]["instances"]) + _logger.debug("instances = %s", json.dumps(instances)) + except InvalidStatusCode as e: + # instance is stopped + self.assertEquals(e.status_code, HTTPStatus.BAD_REQUEST) + self.assertIsInstance(e.body, dict) + self.assertEqual(e.body.get("error_code"), "CF-InstancesError") + try: + stats = client.v2.apps.get_stats(application["metadata"]["guid"]) + self.assertIsNotNone(stats) + self.assertEquals(len(stats), application["entity"]["instances"]) + self.assertEquals(len(stats), application["entity"]["instances"]) + _logger.debug("stats = %s", json.dumps(stats)) + except InvalidStatusCode as e: + # instance is stopped + self.assertEquals(e.status_code, HTTPStatus.BAD_REQUEST) + self.assertIsInstance(e.body, dict) + self.assertEqual(e.body.get("error_code"), "CF-AppStoppedStatsError") + env = client.v2.apps.get_env(application["metadata"]["guid"]) + self.assertIsNotNone(env) + self.assertIsNotNone(env.get("application_env_json")) + self.assertIsNotNone(env["application_env_json"].get("VCAP_APPLICATION")) + self.assertGreater(len(env["application_env_json"]["VCAP_APPLICATION"].get("application_uris", [])), 0) + _logger.debug("env = %s", json.dumps(env)) + cpt += 1 + + _logger.debug("test applications list - %d found", cpt) + + def test_start(self): + client = build_client_from_configuration() + + _logger.debug("start - %s", client.v2.apps.start(client.app_guid, asynchronous=False)) + + def test_stop(self): + client = build_client_from_configuration() + _logger.debug("stop - %s", client.v2.apps.stop(client.app_guid, asynchronous=False)) diff --git a/integration/test_buildpacks.py b/integration/v2/test_buildpacks.py similarity index 85% rename from integration/test_buildpacks.py rename to integration/v2/test_buildpacks.py index 14ae5ee..3953bf9 100644 --- a/integration/test_buildpacks.py +++ b/integration/v2/test_buildpacks.py @@ -10,4 +10,4 @@ class TestBuildpacks(unittest.TestCase): def test_list(self): client = build_client_from_configuration() for buildpack in client.v2.buildpacks.list(): - _logger.debug(' %s' % buildpack.json()) + _logger.debug(" %s" % buildpack.json()) diff --git a/integration/test_loggregator.py b/integration/v2/test_loggregator.py similarity index 79% rename from integration/test_loggregator.py rename to integration/v2/test_loggregator.py index 1dc28ae..7d4b514 100644 --- a/integration/test_loggregator.py +++ b/integration/v2/test_loggregator.py @@ -11,5 +11,5 @@ def test_recent(self): cpt = 0 for log_message in client.loggregator.get_recent(client.log_app_guid): cpt += 1 - _logger.debug('read %s', str(log_message)) - _logger.debug('read %d', cpt) + _logger.debug("read %s", str(log_message)) + _logger.debug("read %d", cpt) diff --git a/integration/test_navigation.py b/integration/v2/test_navigation.py similarity index 68% rename from integration/test_navigation.py rename to integration/v2/test_navigation.py index 03cdea3..b74ab41 100644 --- a/integration/test_navigation.py +++ b/integration/v2/test_navigation.py @@ -10,15 +10,15 @@ class TestNavigation(unittest.TestCase): def test_all(self): client = build_client_from_configuration() for organization in client.v2.organizations: - if organization['metadata']['guid'] == client.org_guid: + if organization["metadata"]["guid"] == client.org_guid: for space in organization.spaces(): - if space['metadata']['guid'] == client.space_guid: + if space["metadata"]["guid"] == client.space_guid: organization_reloaded = space.organization() - self.assertEqual(organization['metadata']['guid'], organization_reloaded['metadata']['guid']) + self.assertEqual(organization["metadata"]["guid"], organization_reloaded["metadata"]["guid"]) for application in space.apps(): - if application['metadata']['guid'] == client.app_guid: + if application["metadata"]["guid"] == client.app_guid: space_reloaded = application.space() - self.assertEqual(space['metadata']['guid'], space_reloaded['metadata']['guid']) + self.assertEqual(space["metadata"]["guid"], space_reloaded["metadata"]["guid"]) application.start() application.stats() application.instances() @@ -32,21 +32,23 @@ def test_all(self): application.stop() for service_instance in space.service_instances(): space_reloaded = service_instance.space() - self.assertEqual(space['metadata']['guid'], space_reloaded['metadata']['guid']) + self.assertEqual(space["metadata"]["guid"], space_reloaded["metadata"]["guid"]) for service_binding in service_instance.service_bindings(): service_instance_reloaded = service_binding.service_instance() - self.assertEqual(service_instance['metadata']['guid'], - service_instance_reloaded['metadata']['guid']) + self.assertEqual( + service_instance["metadata"]["guid"], service_instance_reloaded["metadata"]["guid"] + ) service_binding.app() break for route in service_instance.routes(): service_instance_reloaded = route.service_instance() - self.assertEqual(service_instance['metadata']['guid'], - service_instance_reloaded['metadata']['guid']) + self.assertEqual( + service_instance["metadata"]["guid"], service_instance_reloaded["metadata"]["guid"] + ) for _ in route.apps(): break space_reloaded = route.space() - self.assertEqual(space['metadata']['guid'], space_reloaded['metadata']['guid']) + self.assertEqual(space["metadata"]["guid"], space_reloaded["metadata"]["guid"]) break service_plan = service_instance.service_plan() for _ in service_plan.service_instances(): diff --git a/integration/test_organizations.py b/integration/v2/test_organizations.py similarity index 83% rename from integration/test_organizations.py rename to integration/v2/test_organizations.py index 61cc3ae..5876434 100644 --- a/integration/test_organizations.py +++ b/integration/v2/test_organizations.py @@ -11,10 +11,10 @@ def test_list(self): client = build_client_from_configuration() for organization in client.v2.organizations.list(): if cpt == 0: - organization = client.v2.organizations.get(organization['metadata']['guid']) + organization = client.v2.organizations.get(organization["metadata"]["guid"]) self.assertIsNotNone(organization) - organization = client.v2.organizations.get_first(name=organization['entity']['name']) + organization = client.v2.organizations.get_first(name=organization["entity"]["name"]) self.assertIsNotNone(organization) _logger.debug(organization.json()) cpt += 1 - _logger.debug('test organization list - %d found', cpt) + _logger.debug("test organization list - %d found", cpt) diff --git a/integration/test_routes.py b/integration/v2/test_routes.py similarity index 85% rename from integration/test_routes.py rename to integration/v2/test_routes.py index 0e7c60f..9bbf0f9 100644 --- a/integration/test_routes.py +++ b/integration/v2/test_routes.py @@ -10,4 +10,4 @@ class TestRoutes(unittest.TestCase): def test_list(self): client = build_client_from_configuration() for route in client.v2.routes.list(): - _logger.debug(' %s' % route.json()) + _logger.debug(" %s" % route.json()) diff --git a/integration/test_service_bindings.py b/integration/v2/test_service_bindings.py similarity index 52% rename from integration/test_service_bindings.py rename to integration/v2/test_service_bindings.py index 7cd9017..fdd10b5 100644 --- a/integration/test_service_bindings.py +++ b/integration/v2/test_service_bindings.py @@ -11,19 +11,20 @@ class TestServiceBinding(unittest.TestCase): def test_create_bind_unbind_delete(self): client = build_client_from_configuration() try: - instance = client.v2.service_instances.create(client.space_guid, 'test_name', client.plan_guid, - client.creation_parameters) - except: + instance = client.v2.service_instances.create( + client.space_guid, "test_name", client.plan_guid, client.creation_parameters + ) + except: # noqa: E722 return try: - binding = client.v2.service_bindings.create(client.app_guid, instance['metadata']['guid']) + binding = client.v2.service_bindings.create(client.app_guid, instance["metadata"]["guid"]) _logger.debug(binding.json()) - client.v2.service_bindings.remove(binding['metadata']['guid']) + client.v2.service_bindings.remove(binding["metadata"]["guid"]) _logger.debug("binding deleted") finally: try: - client.v2.service_instances.remove(instance['metadata']['guid']) - except: + client.v2.service_instances.remove(instance["metadata"]["guid"]) + except: # noqa: E722 pass def test_get(self): @@ -33,10 +34,9 @@ def test_get(self): if cpt == 0: _logger.debug(binding) self.assertIsNotNone( - client.v2.service_bindings.get_first(service_instance_guid=binding['entity']['service_instance_guid'])) - self.assertIsNotNone( - client.v2.service_bindings.get_first(app_guid=binding['entity']['app_guid'])) - self.assertIsNotNone( - client.v2.service_bindings.get(binding['metadata']['guid'])) + client.v2.service_bindings.get_first(service_instance_guid=binding["entity"]["service_instance_guid"]) + ) + self.assertIsNotNone(client.v2.service_bindings.get_first(app_guid=binding["entity"]["app_guid"])) + self.assertIsNotNone(client.v2.service_bindings.get(binding["metadata"]["guid"])) cpt += 1 - _logger.debug('test_get - %d found', cpt) + _logger.debug("test_get - %d found", cpt) diff --git a/integration/test_service_brokers.py b/integration/v2/test_service_brokers.py similarity index 56% rename from integration/test_service_brokers.py rename to integration/v2/test_service_brokers.py index 6379149..6ea4f4a 100644 --- a/integration/test_service_brokers.py +++ b/integration/v2/test_service_brokers.py @@ -12,10 +12,8 @@ def test_list(self): client = build_client_from_configuration() for broker in client.v2.service_brokers.list(): if cpt == 0: - self.assertIsNotNone( - client.v2.service_brokers.get_first(space_guid=broker['entity']['space_guid'])) - self.assertIsNotNone( - client.v2.service_brokers.get(broker['metadata']['guid'])) + self.assertIsNotNone(client.v2.service_brokers.get_first(space_guid=broker["entity"]["space_guid"])) + self.assertIsNotNone(client.v2.service_brokers.get(broker["metadata"]["guid"])) cpt += 1 _logger.debug(broker) - _logger.debug('test broker list - %d found', cpt) + _logger.debug("test broker list - %d found", cpt) diff --git a/integration/v2/test_service_instances.py b/integration/v2/test_service_instances.py new file mode 100644 index 0000000..4c31c44 --- /dev/null +++ b/integration/v2/test_service_instances.py @@ -0,0 +1,28 @@ +import logging +import unittest + +from config_test import build_client_from_configuration + +_logger = logging.getLogger(__name__) + + +class TestServiceInstances(unittest.TestCase): + def test_create_update_delete(self): + client = build_client_from_configuration() + result = client.v2.service_instances.create(client.space_guid, "test_name", client.plan_guid, client.creation_parameters) + if len(client.update_parameters) > 0: + client.v2.service_instances.update(result["metadata"]["guid"], client.update_parameters) + else: + _logger.warning("update test skipped") + client.v2.service_instances.remove(result["metadata"]["guid"]) + + def test_get(self): + client = build_client_from_configuration() + cpt = 0 + for instance in client.v2.service_instances.list(): + if cpt == 0: + self.assertIsNotNone(client.v2.service_instances.get_first(space_guid=instance["entity"]["space_guid"])) + self.assertIsNotNone(client.v2.service_instances.get(instance["metadata"]["guid"])) + self.assertIsNotNone(client.v2.service_instances.list_permissions(instance["metadata"]["guid"])) + cpt += 1 + _logger.debug("test_get - %d found", cpt) diff --git a/integration/test_service_keys.py b/integration/v2/test_service_keys.py similarity index 55% rename from integration/test_service_keys.py rename to integration/v2/test_service_keys.py index f092ed0..52ce196 100644 --- a/integration/test_service_keys.py +++ b/integration/v2/test_service_keys.py @@ -10,19 +10,20 @@ class TestServiceKeys(unittest.TestCase): def test_create_delete(self): client = build_client_from_configuration() try: - instance = client.v2.service_instances.create(client.space_guid, 'test_name', client.plan_guid, - client.creation_parameters) - except: + instance = client.v2.service_instances.create( + client.space_guid, "test_name", client.plan_guid, client.creation_parameters + ) + except: # noqa: E722 return try: - service_key = client.v2.service_keys.create(instance['metadata']['guid'], 'test_key_name') + service_key = client.v2.service_keys.create(instance["metadata"]["guid"], "test_key_name") _logger.debug(service_key.json()) - client.v2.service_keys.remove(service_key['metadata']['guid']) + client.v2.service_keys.remove(service_key["metadata"]["guid"]) _logger.debug("service key deleted") finally: try: - client.v2.service_instances.remove(instance['metadata']['guid']) - except: + client.v2.service_instances.remove(instance["metadata"]["guid"]) + except: # noqa: E722 pass def test_get(self): @@ -31,8 +32,8 @@ def test_get(self): for service_key in client.v2.service_keys.list(): if cpt == 0: self.assertIsNotNone( - client.v2.service_keys.get_first(service_instance_guid=service_key['entity']['service_instance_guid'])) - self.assertIsNotNone( - client.v2.service_keys.get(service_key['metadata']['guid'])) + client.v2.service_keys.get_first(service_instance_guid=service_key["entity"]["service_instance_guid"]) + ) + self.assertIsNotNone(client.v2.service_keys.get(service_key["metadata"]["guid"])) cpt += 1 - _logger.debug('test_get - %d found', cpt) + _logger.debug("test_get - %d found", cpt) diff --git a/integration/test_service_plans.py b/integration/v2/test_service_plans.py similarity index 76% rename from integration/test_service_plans.py rename to integration/v2/test_service_plans.py index 09b2f75..97ee56b 100644 --- a/integration/test_service_plans.py +++ b/integration/v2/test_service_plans.py @@ -10,8 +10,7 @@ class TestServicePlan(unittest.TestCase): def test_list_instance_for_plan(self): client = build_client_from_configuration() for instance in client.v2.service_plans.list_instances(client.plan_guid, space_guid=client.space_guid): - _logger.debug('test_list_instance_for_plan - %s -%s', instance['metadata']['guid'], - instance['entity']['name']) + _logger.debug("test_list_instance_for_plan - %s -%s", instance["metadata"]["guid"], instance["entity"]["name"]) def test_list_by_broker(self): cpt = 0 @@ -20,7 +19,7 @@ def test_list_by_broker(self): if cpt == 0: _logger.debug(plan.json()) cpt += 1 - _logger.debug('test plan list - %d found', cpt) + _logger.debug("test plan list - %d found", cpt) def test_list(self): cpt = 0 @@ -28,4 +27,4 @@ def test_list(self): for plan in client.v2.service_plans.list(): _logger.debug(plan.json()) cpt += 1 - _logger.debug('test plan list - %d found', cpt) + _logger.debug("test plan list - %d found", cpt) diff --git a/integration/test_services.py b/integration/v2/test_services.py similarity index 76% rename from integration/test_services.py rename to integration/v2/test_services.py index 28e2fc5..038cbe6 100644 --- a/integration/test_services.py +++ b/integration/v2/test_services.py @@ -10,13 +10,10 @@ def test_list_services(self): cpt = 0 client = build_client_from_configuration() for service in client.v2.services.list(): - _logger.debug('- %s' % service['entity']['label']) + _logger.debug("- %s" % service["entity"]["label"]) if cpt == 0: - service = client.v2.services.get_first(label=service['entity']['label']) + service = client.v2.services.get_first(label=service["entity"]["label"]) self.assertIsNotNone(service) cpt += 1 - _logger.debug('test service list - %d found', cpt) - - - + _logger.debug("test service list - %d found", cpt) diff --git a/integration/test_spaces.py b/integration/v2/test_spaces.py similarity index 68% rename from integration/test_spaces.py rename to integration/v2/test_spaces.py index de0e51f..5d791c0 100644 --- a/integration/test_spaces.py +++ b/integration/v2/test_spaces.py @@ -11,11 +11,11 @@ def test_list(self): cpt = 0 client = build_client_from_configuration() for space in client.v2.spaces.list(organization_guid=client.org_guid): - _logger.debug(' - %s' % space['entity']['name']) + _logger.debug(" - %s" % space["entity"]["name"]) if cpt == 0: - space = client.v2.spaces.get(space['metadata']['guid']) + space = client.v2.spaces.get(space["metadata"]["guid"]) self.assertIsNotNone(space) - space = client.v2.spaces.get_first(organization_guid=client.org_guid, name=space['entity']['name']) + space = client.v2.spaces.get_first(organization_guid=client.org_guid, name=space["entity"]["name"]) self.assertIsNotNone(space) cpt += 1 - _logger.debug('test spaces list - %d found', cpt) + _logger.debug("test spaces list - %d found", cpt) diff --git a/integration/v3/test_service_brokers.py b/integration/v3/test_service_brokers.py new file mode 100644 index 0000000..c407082 --- /dev/null +++ b/integration/v3/test_service_brokers.py @@ -0,0 +1,18 @@ +import logging +import unittest + +from config_test import build_client_from_configuration + +_logger = logging.getLogger(__name__) + + +class TestServiceBrokers(unittest.TestCase): + def test_list(self): + cpt = 0 + client = build_client_from_configuration() + for broker in client.v3.service_brokers.list(): + if cpt == 0: + self.assertIsNotNone(client.v3.service_brokers.get(broker["guid"])) + cpt += 1 + _logger.debug(broker) + _logger.debug("test broker list - %d found", cpt) diff --git a/integration/v3/test_service_instances.py b/integration/v3/test_service_instances.py new file mode 100644 index 0000000..623adec --- /dev/null +++ b/integration/v3/test_service_instances.py @@ -0,0 +1,32 @@ +import logging +import unittest + +from config_test import build_client_from_configuration + +_logger = logging.getLogger(__name__) + + +class TestServiceInstances(unittest.TestCase): + def test_create_delete(self): + client = build_client_from_configuration() + result = client.v3.service_instances.create( + space_guid=client.space_guid, + name="test_name", + service_plan_guid=client.plan_guid, + parameters=client.creation_parameters, + ) + client.v3.jobs.wait_for_job_completion(result.job()["guid"]) + service_instance_guid = client.v3.service_instances.get_first(names="test_name")["guid"] + client.v3.service_instances.remove(service_instance_guid) + + def test_get(self): + client = build_client_from_configuration() + cpt = 0 + for instance in client.v3.service_instances.list(): + if cpt == 0: + self.assertIsNotNone( + client.v3.service_instances.get_first(space_guids=instance["relationships"]["space"]["data"]["guid"]) + ) + self.assertIsNotNone(client.v3.service_instances.get(instance["guid"])) + cpt += 1 + _logger.debug("test_get - %d found", cpt) diff --git a/integration/v3/test_service_plans.py b/integration/v3/test_service_plans.py new file mode 100644 index 0000000..073316d --- /dev/null +++ b/integration/v3/test_service_plans.py @@ -0,0 +1,30 @@ +import logging +import unittest + +from config_test import build_client_from_configuration + +_logger = logging.getLogger(__name__) + + +class TestServicePlan(unittest.TestCase): + def test_list_instance_for_plan(self): + client = build_client_from_configuration() + for instance in client.v3.service_plans.list(service_offering_guids=client.plan_guid, space_guids=client.space_guid): + _logger.debug("test_list_instance_for_plan - %s -%s", instance["metadata"]["guid"], instance["entity"]["name"]) + + def test_list_by_broker(self): + cpt = 0 + client = build_client_from_configuration() + for plan in client.v3.service_plans.list(service_broker_guids=client.service_guid): + if cpt == 0: + _logger.debug(plan.json()) + cpt += 1 + _logger.debug("test plan list - %d found", cpt) + + def test_list(self): + cpt = 0 + client = build_client_from_configuration() + for plan in client.v3.service_plans.list(): + _logger.debug(plan.json()) + cpt += 1 + _logger.debug("test plan list - %d found", cpt) diff --git a/main/cloudfoundry_client/client.py b/main/cloudfoundry_client/client.py deleted file mode 100644 index 7b65784..0000000 --- a/main/cloudfoundry_client/client.py +++ /dev/null @@ -1,201 +0,0 @@ -import logging - -import requests -from oauth2_client.credentials_manager import CredentialManager, ServiceInformation - -from cloudfoundry_client.doppler.client import DopplerClient -from cloudfoundry_client.errors import InvalidStatusCode -from cloudfoundry_client.imported import UNAUTHORIZED -from cloudfoundry_client.v2.apps import AppManager as AppManagerV2 -from cloudfoundry_client.v2.buildpacks import BuildpackManager as BuildpackManagerV2 -from cloudfoundry_client.v2.events import EventManager -from cloudfoundry_client.v2.entities import EntityManager as EntityManagerV2 -from cloudfoundry_client.v2.jobs import JobManager -from cloudfoundry_client.v2.resources import ResourceManager -from cloudfoundry_client.v2.routes import RouteManager -from cloudfoundry_client.v2.service_bindings import ServiceBindingManager -from cloudfoundry_client.v2.service_brokers import ServiceBrokerManager -from cloudfoundry_client.v2.service_instances import ServiceInstanceManager -from cloudfoundry_client.v2.service_keys import ServiceKeyManager -from cloudfoundry_client.v2.service_plan_visibilities import ServicePlanVisibilityManager -from cloudfoundry_client.v2.service_plans import ServicePlanManager -from cloudfoundry_client.v3.apps import AppManager as AppManagerV3 -from cloudfoundry_client.v3.buildpacks import BuildpackManager as BuildpackManagerV3 -from cloudfoundry_client.v3.entities import EntityManager as EntityManagerV3 -from cloudfoundry_client.v3.tasks import TaskManager - -_logger = logging.getLogger(__name__) - - -class Info: - def __init__(self, api_version, authorization_endpoint, api_endpoint, doppler_endpoint): - self.api_version = api_version - self.authorization_endpoint = authorization_endpoint - self.api_endpoint = api_endpoint - self.doppler_endpoint = doppler_endpoint - - -class V2(object): - def __init__(self, target_endpoint, credential_manager): - self.apps = AppManagerV2(target_endpoint, credential_manager) - self.buildpacks = BuildpackManagerV2(target_endpoint, credential_manager) - self.jobs = JobManager(target_endpoint, credential_manager) - self.service_bindings = ServiceBindingManager(target_endpoint, credential_manager) - self.service_brokers = ServiceBrokerManager(target_endpoint, credential_manager) - self.service_instances = ServiceInstanceManager(target_endpoint, credential_manager) - self.service_keys = ServiceKeyManager(target_endpoint, credential_manager) - self.service_plan_visibilities = ServicePlanVisibilityManager(target_endpoint, credential_manager) - self.service_plans = ServicePlanManager(target_endpoint, credential_manager) - # Default implementations - self.event = EventManager(target_endpoint, credential_manager) - self.organizations = EntityManagerV2(target_endpoint, credential_manager, '/v2/organizations') - self.private_domains = EntityManagerV2(target_endpoint, credential_manager, '/v2/private_domains') - self.routes = RouteManager(target_endpoint, credential_manager) - self.services = EntityManagerV2(target_endpoint, credential_manager, '/v2/services') - self.shared_domains = EntityManagerV2(target_endpoint, credential_manager, '/v2/shared_domains') - self.spaces = EntityManagerV2(target_endpoint, credential_manager, '/v2/spaces') - self.stacks = EntityManagerV2(target_endpoint, credential_manager, '/v2/stacks') - self.user_provided_service_instances = EntityManagerV2(target_endpoint, credential_manager, - '/v2/user_provided_service_instances') - self.security_groups = EntityManagerV2( - target_endpoint, credential_manager, '/v2/security_groups') - self.users = EntityManagerV2(target_endpoint, credential_manager, '/v2/users') - # Resources implementation used by push operation - self.resources = ResourceManager(target_endpoint, credential_manager) - - -class V3(object): - def __init__(self, target_endpoint, credential_manager): - self.apps = AppManagerV3(target_endpoint, credential_manager) - self.buildpacks = BuildpackManagerV3(target_endpoint, credential_manager) - self.spaces = EntityManagerV3(target_endpoint, credential_manager, '/v3/spaces') - self.organizations = EntityManagerV3(target_endpoint, credential_manager, '/v3/organizations') - self.service_instances = EntityManagerV3(target_endpoint, credential_manager, '/v3/service_instances') - self.tasks = TaskManager(target_endpoint, credential_manager) - - -class CloudFoundryClient(CredentialManager): - def __init__(self, target_endpoint, client_id='cf', client_secret='', **kwargs): - """" - :param target_endpoint :the target endpoint - :param client_id: the client_id - :param client_secret: the client secret - :param proxy: a dict object with entries http and https - :param verify: parameter directly passed to underlying requests library. - (optional) Either a boolean, in which case it controls whether we verify - the server's TLS certificate, or a string, in which case it must be a path - to a CA bundle to use. Defaults to ``True``. - :param token_format: string Can be set to opaque to retrieve an opaque and revocable token. - See UAA API specifications - :param login_hint: string. Indicates the identity provider to be used. - The passed string has to be a URL-Encoded JSON Object, containing the field origin with value as origin_key - of an identity provider. Note that this identity provider must support the grant type password. - See UAA API specifications - """ - proxy = kwargs.get('proxy', dict(http='', https='')) - verify = kwargs.get('verify', True) - self.token_format = kwargs.get('token_format') - self.login_hint = kwargs.get('login_hint') - info = self._get_info(target_endpoint, proxy, verify=verify) - if not info.api_version.startswith('2.'): - raise AssertionError('Only version 2 is supported for now. Found %s' % info.api_version) - service_information = ServiceInformation(None, '%s/oauth/token' % info.authorization_endpoint, - client_id, client_secret, [], verify) - super(CloudFoundryClient, self).__init__(service_information, proxies=proxy) - self.v2 = V2(target_endpoint, self) - self.v3 = V3(target_endpoint, self) - self._doppler = DopplerClient(info.doppler_endpoint, - self.proxies[ - 'http' if info.doppler_endpoint.startswith('ws://') else 'https'], - self.service_information.verify, - self) if info.doppler_endpoint is not None else None - self.info = info - - @property - def doppler(self): - if self._doppler is None: - raise NotImplementedError('No droppler endpoint for this instance') - else: - - return self._doppler - - @staticmethod - def _get_info(target_endpoint, proxy=None, verify=True): - info_response = CloudFoundryClient._check_response(requests.get('%s/v2/info' % target_endpoint, - proxies=proxy if proxy is not None else dict( - http='', https=''), - verify=verify)) - info = info_response.json() - return Info(info['api_version'], - info['authorization_endpoint'], - target_endpoint, - info.get('doppler_logging_endpoint')) - - @staticmethod - def _is_token_expired(response): - if response.status_code == UNAUTHORIZED: - try: - json_data = response.json() - result = json_data.get('code', 0) == 1000 and json_data.get('error_code', '') == 'CF-InvalidAuthToken' - _logger.info('_is_token_expired - %s' % str(result)) - return result - except Exception as _: - return False - else: - return False - - @staticmethod - def _token_request_headers(_): - return dict(Accept='application/json') - - def __getattr__(self, item): - sub_attr = getattr(self.v2, item, None) - if sub_attr is not None: - return sub_attr - else: - raise AttributeError("type '%s' has no attribute '%s'" % (type(self).__name__, item)) - - def _grant_password_request(self, login, password): - request = super(CloudFoundryClient, self)._grant_password_request(login, password) - if self.token_format is not None: - request['token_format'] = self.token_format - if self.login_hint is not None: - request['login_hint'] = self.login_hint - return request - - def _grant_refresh_token_request(self, refresh_token): - request = super(CloudFoundryClient, self)._grant_refresh_token_request(refresh_token) - if self.token_format is not None: - request['token_format'] = self.token_format - return request - - def get(self, url, params=None, **kwargs): - response = super(CloudFoundryClient, self).get(url, params, **kwargs) - return CloudFoundryClient._check_response(response) - - def post(self, url, data=None, json=None, **kwargs): - response = super(CloudFoundryClient, self).post(url, data, json, **kwargs) - return CloudFoundryClient._check_response(response) - - def put(self, url, data=None, json=None, **kwargs): - response = super(CloudFoundryClient, self).put(url, data, json, **kwargs) - return CloudFoundryClient._check_response(response) - - def patch(self, url, data=None, json=None, **kwargs): - response = super(CloudFoundryClient, self).patch(url, data, json, **kwargs) - return CloudFoundryClient._check_response(response) - - def delete(self, url, **kwargs): - response = super(CloudFoundryClient, self).delete(url, **kwargs) - return CloudFoundryClient._check_response(response) - - @staticmethod - def _check_response(response): - if int(response.status_code / 100) == 2: - return response - else: - try: - body = response.json() - except Exception as _: - body = response.text - raise InvalidStatusCode(response.status_code, body) diff --git a/main/cloudfoundry_client/dropsonde/envelope_pb2.py b/main/cloudfoundry_client/dropsonde/envelope_pb2.py deleted file mode 100644 index 2b55efb..0000000 --- a/main/cloudfoundry_client/dropsonde/envelope_pb2.py +++ /dev/null @@ -1,263 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: envelope.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -import cloudfoundry_client.dropsonde.http_pb2 as http__pb2 -import cloudfoundry_client.dropsonde.log_pb2 as log__pb2 -import cloudfoundry_client.dropsonde.metric_pb2 as metric__pb2 -import cloudfoundry_client.dropsonde.error_pb2 as error__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='envelope.proto', - package='cloudfoundry.dropsonde', - syntax='proto2', - serialized_pb=_b('\n\x0e\x65nvelope.proto\x12\x16\x63loudfoundry.dropsonde\x1a\nhttp.proto\x1a\tlog.proto\x1a\x0cmetric.proto\x1a\x0b\x65rror.proto\"\xde\x05\n\x08\x45nvelope\x12\x0e\n\x06origin\x18\x01 \x02(\t\x12=\n\teventType\x18\x02 \x02(\x0e\x32*.cloudfoundry.dropsonde.Envelope.EventType\x12\x11\n\ttimestamp\x18\x06 \x01(\x03\x12\x12\n\ndeployment\x18\r \x01(\t\x12\x0b\n\x03job\x18\x0e \x01(\t\x12\r\n\x05index\x18\x0f \x01(\t\x12\n\n\x02ip\x18\x10 \x01(\t\x12\x38\n\x04tags\x18\x11 \x03(\x0b\x32*.cloudfoundry.dropsonde.Envelope.TagsEntry\x12<\n\rhttpStartStop\x18\x07 \x01(\x0b\x32%.cloudfoundry.dropsonde.HttpStartStop\x12\x36\n\nlogMessage\x18\x08 \x01(\x0b\x32\".cloudfoundry.dropsonde.LogMessage\x12\x38\n\x0bvalueMetric\x18\t \x01(\x0b\x32#.cloudfoundry.dropsonde.ValueMetric\x12:\n\x0c\x63ounterEvent\x18\n \x01(\x0b\x32$.cloudfoundry.dropsonde.CounterEvent\x12,\n\x05\x65rror\x18\x0b \x01(\x0b\x32\x1d.cloudfoundry.dropsonde.Error\x12@\n\x0f\x63ontainerMetric\x18\x0c \x01(\x0b\x32\'.cloudfoundry.dropsonde.ContainerMetric\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"q\n\tEventType\x12\x11\n\rHttpStartStop\x10\x04\x12\x0e\n\nLogMessage\x10\x05\x12\x0f\n\x0bValueMetric\x10\x06\x12\x10\n\x0c\x43ounterEvent\x10\x07\x12\t\n\x05\x45rror\x10\x08\x12\x13\n\x0f\x43ontainerMetric\x10\tB1\n!org.cloudfoundry.dropsonde.eventsB\x0c\x45ventFactory') - , - dependencies=[http__pb2.DESCRIPTOR,log__pb2.DESCRIPTOR,metric__pb2.DESCRIPTOR,error__pb2.DESCRIPTOR,]) - - - -_ENVELOPE_EVENTTYPE = _descriptor.EnumDescriptor( - name='EventType', - full_name='cloudfoundry.dropsonde.Envelope.EventType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='HttpStartStop', index=0, number=4, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='LogMessage', index=1, number=5, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ValueMetric', index=2, number=6, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='CounterEvent', index=3, number=7, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='Error', index=4, number=8, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ContainerMetric', index=5, number=9, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=714, - serialized_end=827, -) -_sym_db.RegisterEnumDescriptor(_ENVELOPE_EVENTTYPE) - - -_ENVELOPE_TAGSENTRY = _descriptor.Descriptor( - name='TagsEntry', - full_name='cloudfoundry.dropsonde.Envelope.TagsEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='cloudfoundry.dropsonde.Envelope.TagsEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='cloudfoundry.dropsonde.Envelope.TagsEntry.value', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=669, - serialized_end=712, -) - -_ENVELOPE = _descriptor.Descriptor( - name='Envelope', - full_name='cloudfoundry.dropsonde.Envelope', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='origin', full_name='cloudfoundry.dropsonde.Envelope.origin', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='eventType', full_name='cloudfoundry.dropsonde.Envelope.eventType', index=1, - number=2, type=14, cpp_type=8, label=2, - has_default_value=False, default_value=4, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='timestamp', full_name='cloudfoundry.dropsonde.Envelope.timestamp', index=2, - number=6, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='deployment', full_name='cloudfoundry.dropsonde.Envelope.deployment', index=3, - number=13, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='job', full_name='cloudfoundry.dropsonde.Envelope.job', index=4, - number=14, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='index', full_name='cloudfoundry.dropsonde.Envelope.index', index=5, - number=15, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='ip', full_name='cloudfoundry.dropsonde.Envelope.ip', index=6, - number=16, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='tags', full_name='cloudfoundry.dropsonde.Envelope.tags', index=7, - number=17, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='httpStartStop', full_name='cloudfoundry.dropsonde.Envelope.httpStartStop', index=8, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='logMessage', full_name='cloudfoundry.dropsonde.Envelope.logMessage', index=9, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='valueMetric', full_name='cloudfoundry.dropsonde.Envelope.valueMetric', index=10, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='counterEvent', full_name='cloudfoundry.dropsonde.Envelope.counterEvent', index=11, - number=10, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='error', full_name='cloudfoundry.dropsonde.Envelope.error', index=12, - number=11, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='containerMetric', full_name='cloudfoundry.dropsonde.Envelope.containerMetric', index=13, - number=12, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[_ENVELOPE_TAGSENTRY, ], - enum_types=[ - _ENVELOPE_EVENTTYPE, - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=93, - serialized_end=827, -) - -_ENVELOPE_TAGSENTRY.containing_type = _ENVELOPE -_ENVELOPE.fields_by_name['eventType'].enum_type = _ENVELOPE_EVENTTYPE -_ENVELOPE.fields_by_name['tags'].message_type = _ENVELOPE_TAGSENTRY -_ENVELOPE.fields_by_name['httpStartStop'].message_type = http__pb2._HTTPSTARTSTOP -_ENVELOPE.fields_by_name['logMessage'].message_type = log__pb2._LOGMESSAGE -_ENVELOPE.fields_by_name['valueMetric'].message_type = metric__pb2._VALUEMETRIC -_ENVELOPE.fields_by_name['counterEvent'].message_type = metric__pb2._COUNTEREVENT -_ENVELOPE.fields_by_name['error'].message_type = error__pb2._ERROR -_ENVELOPE.fields_by_name['containerMetric'].message_type = metric__pb2._CONTAINERMETRIC -_ENVELOPE_EVENTTYPE.containing_type = _ENVELOPE -DESCRIPTOR.message_types_by_name['Envelope'] = _ENVELOPE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Envelope = _reflection.GeneratedProtocolMessageType('Envelope', (_message.Message,), dict( - - TagsEntry = _reflection.GeneratedProtocolMessageType('TagsEntry', (_message.Message,), dict( - DESCRIPTOR = _ENVELOPE_TAGSENTRY, - __module__ = 'envelope_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.Envelope.TagsEntry) - )) - , - DESCRIPTOR = _ENVELOPE, - __module__ = 'envelope_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.Envelope) - )) -_sym_db.RegisterMessage(Envelope) -_sym_db.RegisterMessage(Envelope.TagsEntry) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n!org.cloudfoundry.dropsonde.eventsB\014EventFactory')) -_ENVELOPE_TAGSENTRY.has_options = True -_ENVELOPE_TAGSENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) -# @@protoc_insertion_point(module_scope) diff --git a/main/cloudfoundry_client/dropsonde/error_pb2.py b/main/cloudfoundry_client/dropsonde/error_pb2.py deleted file mode 100644 index 571058d..0000000 --- a/main/cloudfoundry_client/dropsonde/error_pb2.py +++ /dev/null @@ -1,85 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: error.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='error.proto', - package='cloudfoundry.dropsonde', - syntax='proto2', - serialized_pb=_b('\n\x0b\x65rror.proto\x12\x16\x63loudfoundry.dropsonde\"6\n\x05\x45rror\x12\x0e\n\x06source\x18\x01 \x02(\t\x12\x0c\n\x04\x63ode\x18\x02 \x02(\x05\x12\x0f\n\x07message\x18\x03 \x02(\tB1\n!org.cloudfoundry.dropsonde.eventsB\x0c\x45rrorFactory') -) - - - - -_ERROR = _descriptor.Descriptor( - name='Error', - full_name='cloudfoundry.dropsonde.Error', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='cloudfoundry.dropsonde.Error.source', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='code', full_name='cloudfoundry.dropsonde.Error.code', index=1, - number=2, type=5, cpp_type=1, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='message', full_name='cloudfoundry.dropsonde.Error.message', index=2, - number=3, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=39, - serialized_end=93, -) - -DESCRIPTOR.message_types_by_name['Error'] = _ERROR -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( - DESCRIPTOR = _ERROR, - __module__ = 'error_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.Error) - )) -_sym_db.RegisterMessage(Error) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n!org.cloudfoundry.dropsonde.eventsB\014ErrorFactory')) -# @@protoc_insertion_point(module_scope) diff --git a/main/cloudfoundry_client/dropsonde/http_pb2.py b/main/cloudfoundry_client/dropsonde/http_pb2.py deleted file mode 100644 index 9cc50b8..0000000 --- a/main/cloudfoundry_client/dropsonde/http_pb2.py +++ /dev/null @@ -1,431 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: http.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -import cloudfoundry_client.dropsonde.uuid_pb2 as uuid__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='http.proto', - package='cloudfoundry.dropsonde', - syntax='proto2', - serialized_pb=_b('\n\nhttp.proto\x12\x16\x63loudfoundry.dropsonde\x1a\nuuid.proto\"\xa8\x03\n\rHttpStartStop\x12\x16\n\x0estartTimestamp\x18\x01 \x02(\x03\x12\x15\n\rstopTimestamp\x18\x02 \x02(\x03\x12/\n\trequestId\x18\x03 \x02(\x0b\x32\x1c.cloudfoundry.dropsonde.UUID\x12\x32\n\x08peerType\x18\x04 \x02(\x0e\x32 .cloudfoundry.dropsonde.PeerType\x12.\n\x06method\x18\x05 \x02(\x0e\x32\x1e.cloudfoundry.dropsonde.Method\x12\x0b\n\x03uri\x18\x06 \x02(\t\x12\x15\n\rremoteAddress\x18\x07 \x02(\t\x12\x11\n\tuserAgent\x18\x08 \x02(\t\x12\x12\n\nstatusCode\x18\t \x02(\x05\x12\x15\n\rcontentLength\x18\n \x02(\x03\x12\x33\n\rapplicationId\x18\x0c \x01(\x0b\x32\x1c.cloudfoundry.dropsonde.UUID\x12\x15\n\rinstanceIndex\x18\r \x01(\x05\x12\x12\n\ninstanceId\x18\x0e \x01(\t\x12\x11\n\tforwarded\x18\x0f \x03(\t*\"\n\x08PeerType\x12\n\n\x06\x43lient\x10\x01\x12\n\n\x06Server\x10\x02*\xc6\x04\n\x06Method\x12\x07\n\x03GET\x10\x01\x12\x08\n\x04POST\x10\x02\x12\x07\n\x03PUT\x10\x03\x12\n\n\x06\x44\x45LETE\x10\x04\x12\x08\n\x04HEAD\x10\x05\x12\x07\n\x03\x41\x43L\x10\x06\x12\x14\n\x10\x42\x41SELINE_CONTROL\x10\x07\x12\x08\n\x04\x42IND\x10\x08\x12\x0b\n\x07\x43HECKIN\x10\t\x12\x0c\n\x08\x43HECKOUT\x10\n\x12\x0b\n\x07\x43ONNECT\x10\x0b\x12\x08\n\x04\x43OPY\x10\x0c\x12\t\n\x05\x44\x45\x42UG\x10\r\x12\t\n\x05LABEL\x10\x0e\x12\x08\n\x04LINK\x10\x0f\x12\x08\n\x04LOCK\x10\x10\x12\t\n\x05MERGE\x10\x11\x12\x0e\n\nMKACTIVITY\x10\x12\x12\x0e\n\nMKCALENDAR\x10\x13\x12\t\n\x05MKCOL\x10\x14\x12\x11\n\rMKREDIRECTREF\x10\x15\x12\x0f\n\x0bMKWORKSPACE\x10\x16\x12\x08\n\x04MOVE\x10\x17\x12\x0b\n\x07OPTIONS\x10\x18\x12\x0e\n\nORDERPATCH\x10\x19\x12\t\n\x05PATCH\x10\x1a\x12\x07\n\x03PRI\x10\x1b\x12\x0c\n\x08PROPFIND\x10\x1c\x12\r\n\tPROPPATCH\x10\x1d\x12\n\n\x06REBIND\x10\x1e\x12\n\n\x06REPORT\x10\x1f\x12\n\n\x06SEARCH\x10 \x12\x0e\n\nSHOWMETHOD\x10!\x12\r\n\tSPACEJUMP\x10\"\x12\x0e\n\nTEXTSEARCH\x10#\x12\t\n\x05TRACE\x10$\x12\t\n\x05TRACK\x10%\x12\n\n\x06UNBIND\x10&\x12\x0e\n\nUNCHECKOUT\x10\'\x12\n\n\x06UNLINK\x10(\x12\n\n\x06UNLOCK\x10)\x12\n\n\x06UPDATE\x10*\x12\x15\n\x11UPDATEREDIRECTREF\x10+\x12\x13\n\x0fVERSION_CONTROL\x10,B0\n!org.cloudfoundry.dropsonde.eventsB\x0bHttpFactory') - , - dependencies=[uuid__pb2.DESCRIPTOR,]) - -_PEERTYPE = _descriptor.EnumDescriptor( - name='PeerType', - full_name='cloudfoundry.dropsonde.PeerType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='Client', index=0, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='Server', index=1, number=2, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=477, - serialized_end=511, -) -_sym_db.RegisterEnumDescriptor(_PEERTYPE) - -PeerType = enum_type_wrapper.EnumTypeWrapper(_PEERTYPE) -_METHOD = _descriptor.EnumDescriptor( - name='Method', - full_name='cloudfoundry.dropsonde.Method', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='GET', index=0, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='POST', index=1, number=2, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='PUT', index=2, number=3, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DELETE', index=3, number=4, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='HEAD', index=4, number=5, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ACL', index=5, number=6, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='BASELINE_CONTROL', index=6, number=7, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='BIND', index=7, number=8, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='CHECKIN', index=8, number=9, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='CHECKOUT', index=9, number=10, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='CONNECT', index=10, number=11, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='COPY', index=11, number=12, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DEBUG', index=12, number=13, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='LABEL', index=13, number=14, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='LINK', index=14, number=15, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='LOCK', index=15, number=16, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MERGE', index=16, number=17, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MKACTIVITY', index=17, number=18, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MKCALENDAR', index=18, number=19, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MKCOL', index=19, number=20, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MKREDIRECTREF', index=20, number=21, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MKWORKSPACE', index=21, number=22, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='MOVE', index=22, number=23, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='OPTIONS', index=23, number=24, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ORDERPATCH', index=24, number=25, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='PATCH', index=25, number=26, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='PRI', index=26, number=27, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='PROPFIND', index=27, number=28, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='PROPPATCH', index=28, number=29, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='REBIND', index=29, number=30, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='REPORT', index=30, number=31, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='SEARCH', index=31, number=32, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='SHOWMETHOD', index=32, number=33, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='SPACEJUMP', index=33, number=34, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TEXTSEARCH', index=34, number=35, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRACE', index=35, number=36, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='TRACK', index=36, number=37, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='UNBIND', index=37, number=38, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='UNCHECKOUT', index=38, number=39, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='UNLINK', index=39, number=40, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='UNLOCK', index=40, number=41, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='UPDATE', index=41, number=42, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='UPDATEREDIRECTREF', index=42, number=43, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='VERSION_CONTROL', index=43, number=44, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=514, - serialized_end=1096, -) -_sym_db.RegisterEnumDescriptor(_METHOD) - -Method = enum_type_wrapper.EnumTypeWrapper(_METHOD) -Client = 1 -Server = 2 -GET = 1 -POST = 2 -PUT = 3 -DELETE = 4 -HEAD = 5 -ACL = 6 -BASELINE_CONTROL = 7 -BIND = 8 -CHECKIN = 9 -CHECKOUT = 10 -CONNECT = 11 -COPY = 12 -DEBUG = 13 -LABEL = 14 -LINK = 15 -LOCK = 16 -MERGE = 17 -MKACTIVITY = 18 -MKCALENDAR = 19 -MKCOL = 20 -MKREDIRECTREF = 21 -MKWORKSPACE = 22 -MOVE = 23 -OPTIONS = 24 -ORDERPATCH = 25 -PATCH = 26 -PRI = 27 -PROPFIND = 28 -PROPPATCH = 29 -REBIND = 30 -REPORT = 31 -SEARCH = 32 -SHOWMETHOD = 33 -SPACEJUMP = 34 -TEXTSEARCH = 35 -TRACE = 36 -TRACK = 37 -UNBIND = 38 -UNCHECKOUT = 39 -UNLINK = 40 -UNLOCK = 41 -UPDATE = 42 -UPDATEREDIRECTREF = 43 -VERSION_CONTROL = 44 - - - -_HTTPSTARTSTOP = _descriptor.Descriptor( - name='HttpStartStop', - full_name='cloudfoundry.dropsonde.HttpStartStop', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='startTimestamp', full_name='cloudfoundry.dropsonde.HttpStartStop.startTimestamp', index=0, - number=1, type=3, cpp_type=2, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='stopTimestamp', full_name='cloudfoundry.dropsonde.HttpStartStop.stopTimestamp', index=1, - number=2, type=3, cpp_type=2, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='requestId', full_name='cloudfoundry.dropsonde.HttpStartStop.requestId', index=2, - number=3, type=11, cpp_type=10, label=2, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='peerType', full_name='cloudfoundry.dropsonde.HttpStartStop.peerType', index=3, - number=4, type=14, cpp_type=8, label=2, - has_default_value=False, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='method', full_name='cloudfoundry.dropsonde.HttpStartStop.method', index=4, - number=5, type=14, cpp_type=8, label=2, - has_default_value=False, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='uri', full_name='cloudfoundry.dropsonde.HttpStartStop.uri', index=5, - number=6, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='remoteAddress', full_name='cloudfoundry.dropsonde.HttpStartStop.remoteAddress', index=6, - number=7, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='userAgent', full_name='cloudfoundry.dropsonde.HttpStartStop.userAgent', index=7, - number=8, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='statusCode', full_name='cloudfoundry.dropsonde.HttpStartStop.statusCode', index=8, - number=9, type=5, cpp_type=1, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='contentLength', full_name='cloudfoundry.dropsonde.HttpStartStop.contentLength', index=9, - number=10, type=3, cpp_type=2, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='applicationId', full_name='cloudfoundry.dropsonde.HttpStartStop.applicationId', index=10, - number=12, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='instanceIndex', full_name='cloudfoundry.dropsonde.HttpStartStop.instanceIndex', index=11, - number=13, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='instanceId', full_name='cloudfoundry.dropsonde.HttpStartStop.instanceId', index=12, - number=14, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='forwarded', full_name='cloudfoundry.dropsonde.HttpStartStop.forwarded', index=13, - number=15, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=51, - serialized_end=475, -) - -_HTTPSTARTSTOP.fields_by_name['requestId'].message_type = uuid__pb2._UUID -_HTTPSTARTSTOP.fields_by_name['peerType'].enum_type = _PEERTYPE -_HTTPSTARTSTOP.fields_by_name['method'].enum_type = _METHOD -_HTTPSTARTSTOP.fields_by_name['applicationId'].message_type = uuid__pb2._UUID -DESCRIPTOR.message_types_by_name['HttpStartStop'] = _HTTPSTARTSTOP -DESCRIPTOR.enum_types_by_name['PeerType'] = _PEERTYPE -DESCRIPTOR.enum_types_by_name['Method'] = _METHOD -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -HttpStartStop = _reflection.GeneratedProtocolMessageType('HttpStartStop', (_message.Message,), dict( - DESCRIPTOR = _HTTPSTARTSTOP, - __module__ = 'http_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.HttpStartStop) - )) -_sym_db.RegisterMessage(HttpStartStop) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n!org.cloudfoundry.dropsonde.eventsB\013HttpFactory')) -# @@protoc_insertion_point(module_scope) diff --git a/main/cloudfoundry_client/dropsonde/log_pb2.py b/main/cloudfoundry_client/dropsonde/log_pb2.py deleted file mode 100644 index 371a196..0000000 --- a/main/cloudfoundry_client/dropsonde/log_pb2.py +++ /dev/null @@ -1,131 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: log.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='log.proto', - package='cloudfoundry.dropsonde', - syntax='proto2', - serialized_pb=_b('\n\tlog.proto\x12\x16\x63loudfoundry.dropsonde\"\xd5\x01\n\nLogMessage\x12\x0f\n\x07message\x18\x01 \x02(\x0c\x12\x44\n\x0cmessage_type\x18\x02 \x02(\x0e\x32..cloudfoundry.dropsonde.LogMessage.MessageType\x12\x11\n\ttimestamp\x18\x03 \x02(\x03\x12\x0e\n\x06\x61pp_id\x18\x04 \x01(\t\x12\x13\n\x0bsource_type\x18\x05 \x01(\t\x12\x17\n\x0fsource_instance\x18\x06 \x01(\t\"\x1f\n\x0bMessageType\x12\x07\n\x03OUT\x10\x01\x12\x07\n\x03\x45RR\x10\x02\x42/\n!org.cloudfoundry.dropsonde.eventsB\nLogFactory') -) - - - -_LOGMESSAGE_MESSAGETYPE = _descriptor.EnumDescriptor( - name='MessageType', - full_name='cloudfoundry.dropsonde.LogMessage.MessageType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='OUT', index=0, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ERR', index=1, number=2, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=220, - serialized_end=251, -) -_sym_db.RegisterEnumDescriptor(_LOGMESSAGE_MESSAGETYPE) - - -_LOGMESSAGE = _descriptor.Descriptor( - name='LogMessage', - full_name='cloudfoundry.dropsonde.LogMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='message', full_name='cloudfoundry.dropsonde.LogMessage.message', index=0, - number=1, type=12, cpp_type=9, label=2, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='message_type', full_name='cloudfoundry.dropsonde.LogMessage.message_type', index=1, - number=2, type=14, cpp_type=8, label=2, - has_default_value=False, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='timestamp', full_name='cloudfoundry.dropsonde.LogMessage.timestamp', index=2, - number=3, type=3, cpp_type=2, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='app_id', full_name='cloudfoundry.dropsonde.LogMessage.app_id', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='source_type', full_name='cloudfoundry.dropsonde.LogMessage.source_type', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='source_instance', full_name='cloudfoundry.dropsonde.LogMessage.source_instance', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _LOGMESSAGE_MESSAGETYPE, - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=38, - serialized_end=251, -) - -_LOGMESSAGE.fields_by_name['message_type'].enum_type = _LOGMESSAGE_MESSAGETYPE -_LOGMESSAGE_MESSAGETYPE.containing_type = _LOGMESSAGE -DESCRIPTOR.message_types_by_name['LogMessage'] = _LOGMESSAGE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -LogMessage = _reflection.GeneratedProtocolMessageType('LogMessage', (_message.Message,), dict( - DESCRIPTOR = _LOGMESSAGE, - __module__ = 'log_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.LogMessage) - )) -_sym_db.RegisterMessage(LogMessage) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n!org.cloudfoundry.dropsonde.eventsB\nLogFactory')) -# @@protoc_insertion_point(module_scope) diff --git a/main/cloudfoundry_client/dropsonde/metric_pb2.py b/main/cloudfoundry_client/dropsonde/metric_pb2.py deleted file mode 100644 index 76d7f11..0000000 --- a/main/cloudfoundry_client/dropsonde/metric_pb2.py +++ /dev/null @@ -1,221 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: metric.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -import cloudfoundry_client.dropsonde.uuid_pb2 as uuid__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='metric.proto', - package='cloudfoundry.dropsonde', - syntax='proto2', - serialized_pb=_b('\n\x0cmetric.proto\x12\x16\x63loudfoundry.dropsonde\x1a\nuuid.proto\"8\n\x0bValueMetric\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\r\n\x05value\x18\x02 \x02(\x01\x12\x0c\n\x04unit\x18\x03 \x02(\t\":\n\x0c\x43ounterEvent\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\r\n\x05\x64\x65lta\x18\x02 \x02(\x04\x12\r\n\x05total\x18\x03 \x01(\x04\"\xb0\x01\n\x0f\x43ontainerMetric\x12\x15\n\rapplicationId\x18\x01 \x02(\t\x12\x15\n\rinstanceIndex\x18\x02 \x02(\x05\x12\x15\n\rcpuPercentage\x18\x03 \x02(\x01\x12\x13\n\x0bmemoryBytes\x18\x04 \x02(\x04\x12\x11\n\tdiskBytes\x18\x05 \x02(\x04\x12\x18\n\x10memoryBytesQuota\x18\x06 \x01(\x04\x12\x16\n\x0e\x64iskBytesQuota\x18\x07 \x01(\x04\x42\x32\n!org.cloudfoundry.dropsonde.eventsB\rMetricFactory') - , - dependencies=[uuid__pb2.DESCRIPTOR,]) - - - - -_VALUEMETRIC = _descriptor.Descriptor( - name='ValueMetric', - full_name='cloudfoundry.dropsonde.ValueMetric', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='cloudfoundry.dropsonde.ValueMetric.name', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='value', full_name='cloudfoundry.dropsonde.ValueMetric.value', index=1, - number=2, type=1, cpp_type=5, label=2, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='unit', full_name='cloudfoundry.dropsonde.ValueMetric.unit', index=2, - number=3, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=52, - serialized_end=108, -) - - -_COUNTEREVENT = _descriptor.Descriptor( - name='CounterEvent', - full_name='cloudfoundry.dropsonde.CounterEvent', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='cloudfoundry.dropsonde.CounterEvent.name', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='delta', full_name='cloudfoundry.dropsonde.CounterEvent.delta', index=1, - number=2, type=4, cpp_type=4, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='total', full_name='cloudfoundry.dropsonde.CounterEvent.total', index=2, - number=3, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=110, - serialized_end=168, -) - - -_CONTAINERMETRIC = _descriptor.Descriptor( - name='ContainerMetric', - full_name='cloudfoundry.dropsonde.ContainerMetric', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='applicationId', full_name='cloudfoundry.dropsonde.ContainerMetric.applicationId', index=0, - number=1, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='instanceIndex', full_name='cloudfoundry.dropsonde.ContainerMetric.instanceIndex', index=1, - number=2, type=5, cpp_type=1, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='cpuPercentage', full_name='cloudfoundry.dropsonde.ContainerMetric.cpuPercentage', index=2, - number=3, type=1, cpp_type=5, label=2, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='memoryBytes', full_name='cloudfoundry.dropsonde.ContainerMetric.memoryBytes', index=3, - number=4, type=4, cpp_type=4, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='diskBytes', full_name='cloudfoundry.dropsonde.ContainerMetric.diskBytes', index=4, - number=5, type=4, cpp_type=4, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='memoryBytesQuota', full_name='cloudfoundry.dropsonde.ContainerMetric.memoryBytesQuota', index=5, - number=6, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='diskBytesQuota', full_name='cloudfoundry.dropsonde.ContainerMetric.diskBytesQuota', index=6, - number=7, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=171, - serialized_end=347, -) - -DESCRIPTOR.message_types_by_name['ValueMetric'] = _VALUEMETRIC -DESCRIPTOR.message_types_by_name['CounterEvent'] = _COUNTEREVENT -DESCRIPTOR.message_types_by_name['ContainerMetric'] = _CONTAINERMETRIC -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -ValueMetric = _reflection.GeneratedProtocolMessageType('ValueMetric', (_message.Message,), dict( - DESCRIPTOR = _VALUEMETRIC, - __module__ = 'metric_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.ValueMetric) - )) -_sym_db.RegisterMessage(ValueMetric) - -CounterEvent = _reflection.GeneratedProtocolMessageType('CounterEvent', (_message.Message,), dict( - DESCRIPTOR = _COUNTEREVENT, - __module__ = 'metric_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.CounterEvent) - )) -_sym_db.RegisterMessage(CounterEvent) - -ContainerMetric = _reflection.GeneratedProtocolMessageType('ContainerMetric', (_message.Message,), dict( - DESCRIPTOR = _CONTAINERMETRIC, - __module__ = 'metric_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.ContainerMetric) - )) -_sym_db.RegisterMessage(ContainerMetric) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n!org.cloudfoundry.dropsonde.eventsB\rMetricFactory')) -# @@protoc_insertion_point(module_scope) diff --git a/main/cloudfoundry_client/dropsonde/uuid_pb2.py b/main/cloudfoundry_client/dropsonde/uuid_pb2.py deleted file mode 100644 index 547aa21..0000000 --- a/main/cloudfoundry_client/dropsonde/uuid_pb2.py +++ /dev/null @@ -1,78 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: uuid.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='uuid.proto', - package='cloudfoundry.dropsonde', - syntax='proto2', - serialized_pb=_b('\n\nuuid.proto\x12\x16\x63loudfoundry.dropsonde\"!\n\x04UUID\x12\x0b\n\x03low\x18\x01 \x02(\x04\x12\x0c\n\x04high\x18\x02 \x02(\x04\x42\x30\n!org.cloudfoundry.dropsonde.eventsB\x0bUuidFactory') -) - - - - -_UUID = _descriptor.Descriptor( - name='UUID', - full_name='cloudfoundry.dropsonde.UUID', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='low', full_name='cloudfoundry.dropsonde.UUID.low', index=0, - number=1, type=4, cpp_type=4, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='high', full_name='cloudfoundry.dropsonde.UUID.high', index=1, - number=2, type=4, cpp_type=4, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=38, - serialized_end=71, -) - -DESCRIPTOR.message_types_by_name['UUID'] = _UUID -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -UUID = _reflection.GeneratedProtocolMessageType('UUID', (_message.Message,), dict( - DESCRIPTOR = _UUID, - __module__ = 'uuid_pb2' - # @@protoc_insertion_point(class_scope:cloudfoundry.dropsonde.UUID) - )) -_sym_db.RegisterMessage(UUID) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n!org.cloudfoundry.dropsonde.eventsB\013UuidFactory')) -# @@protoc_insertion_point(module_scope) diff --git a/main/cloudfoundry_client/errors.py b/main/cloudfoundry_client/errors.py deleted file mode 100644 index 5d9f498..0000000 --- a/main/cloudfoundry_client/errors.py +++ /dev/null @@ -1,28 +0,0 @@ -import json - - -class InvalidLogResponseException(Exception): - pass - - -class InvalidStatusCode(Exception): - def __init__(self, status_code, body): - self.status_code = status_code - self.body = body - - def __str__(self): - if self.body is None: - return '%d' % self.status_code - elif type(self.body) == str: - return '%d : %s' % (self.status_code, self.body) - else: - return '%d : %s' % (self.status_code, json.dumps(self.body)) - - -class InvalidEntity(Exception): - def __init__(self, **kwargs): - super(InvalidEntity, self).__init__() - self.raw_entity = dict(**kwargs) - - def __str__(self): - return 'InvalidEntity: %s' % json.dumps(self.raw_entity) diff --git a/main/cloudfoundry_client/imported.py b/main/cloudfoundry_client/imported.py deleted file mode 100644 index 9cb4b4b..0000000 --- a/main/cloudfoundry_client/imported.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys - -import requests - -if sys.version_info.major == 2: - - from httplib import UNAUTHORIZED, BAD_REQUEST, NOT_FOUND, OK - from urllib import quote - from urlparse import urlparse - - requests.packages.urllib3.disable_warnings() - from __builtin__ import reduce - - def bufferize_string(content): - return content -elif sys.version_info.major == 3: - from http import HTTPStatus - UNAUTHORIZED = HTTPStatus.UNAUTHORIZED.value - BAD_REQUEST = HTTPStatus.BAD_REQUEST.value - NOT_FOUND = HTTPStatus.NOT_FOUND.value - OK = HTTPStatus.OK.value - from urllib.parse import quote, urlparse - from functools import reduce - - def bufferize_string(content): - return bytes(content, 'UTF-8') - -else: - raise ImportError('Invalid major version: %d' % sys.version_info.major) diff --git a/main/cloudfoundry_client/json_object.py b/main/cloudfoundry_client/json_object.py deleted file mode 100644 index 0de6b56..0000000 --- a/main/cloudfoundry_client/json_object.py +++ /dev/null @@ -1,8 +0,0 @@ -import json - - -class JsonObject(dict): - def __init__(self, *args, **kwargs): - super(JsonObject, self).__init__(*args, **kwargs) - - json = json.dumps \ No newline at end of file diff --git a/main/cloudfoundry_client/main/apps_command_domain.py b/main/cloudfoundry_client/main/apps_command_domain.py deleted file mode 100644 index e3f97a7..0000000 --- a/main/cloudfoundry_client/main/apps_command_domain.py +++ /dev/null @@ -1,71 +0,0 @@ -from cloudfoundry_client.main.command_domain import CommandDomain, Command - - -class AppCommandDomain(CommandDomain): - def __init__(self): - super(AppCommandDomain, self).__init__(display_name='Applications', - entity_name='app', - filter_list_parameters=['organization_guid', 'space_guid'], - allow_retrieve_by_name=True, - allow_deletion=True, - extra_methods=[(self.recent_logs(), 'Recent Logs',), - (self.stream_logs(), 'Stream Logs',), - (self.simple_extra_command('env'), - 'Get the environment of an application'), - (self.simple_extra_command('instances'), - 'Get the instances of an application',), - (self.simple_extra_command('stats'), - 'Get the stats of an application',), - (self.simple_extra_command('summary'), - 'Get the summary of an application',), - (self.simple_extra_command('start'), - 'Start an application',), - (self.simple_extra_command('stop'), - 'Stop an application',), - (self.simple_extra_command('restage'), - 'Restage an application',), - (self.app_routes(), - 'List the routes(host) of an application')]) - - def recent_logs(self): - def execute(client, arguments): - resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) - for envelope in client.doppler.recent_logs(resource_id): - print(envelope) - - return Command('recent_logs', self._generate_id_command_parser('recent_logs'), execute) - - def stream_logs(self): - def execute(client, arguments): - resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) - try: - for envelope in client.doppler.stream_logs(resource_id): - print(envelope) - except KeyboardInterrupt: - pass - - return Command('stream_logs', self._generate_id_command_parser('stream_logs'), execute) - - def simple_extra_command(self, entry): - def execute(client, arguments): - resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) - print(getattr(self._get_client_domain(client), entry)(resource_id).json(indent=1)) - - return Command(entry, self._generate_id_command_parser(entry), execute) - - def app_routes(self): - def execute(client, arguments): - resource_id = self.resolve_id(arguments.id[0], lambda x: self._get_client_domain(client).get_first(name=x)) - for entity in getattr(self._get_client_domain(client), 'list_routes')(resource_id): - print('%s - %s' % (entity['metadata']['guid'], entity['entity']['host'])) - - return Command('app_routes', self._generate_id_command_parser('app_routes'), execute) - - @staticmethod - def _generate_id_command_parser(entry): - def generate_parser(parser): - command_parser = parser.add_parser(entry) - command_parser.add_argument('id', metavar='ids', type=str, nargs=1, - help='The id. Can be UUID or name (first found then)') - - return generate_parser diff --git a/main/cloudfoundry_client/main/command_domain.py b/main/cloudfoundry_client/main/command_domain.py deleted file mode 100644 index c74c27f..0000000 --- a/main/cloudfoundry_client/main/command_domain.py +++ /dev/null @@ -1,213 +0,0 @@ -import functools -import json -import os -import re -from collections import OrderedDict - -from cloudfoundry_client.errors import InvalidStatusCode -from cloudfoundry_client.imported import NOT_FOUND - - -class Command(object): - def __init__(self, entry, generate_parser, execute): - self.entry = entry - self.generate_parser = generate_parser - self.execute = execute - - -class CommandDomain(object): - def __init__(self, display_name, entity_name, filter_list_parameters, - api_version='v2', name_property='name', - allow_retrieve_by_name=False, allow_creation=False, allow_deletion=False, - extra_methods=None): - self.display_name = display_name - self.client_domain = self.plural(entity_name) - self.api_version = api_version - self.entity_name = entity_name - self.filter_list_parameters = filter_list_parameters - self.name_property = name_property - self.allow_retrieve_by_name = allow_retrieve_by_name - self.allow_creation = allow_creation - self.allow_deletion = allow_deletion - - self.commands = OrderedDict() - self.commands[self._list_entry()] = self.list() - self.commands[self._get_entry()] = self.get() - if self.allow_creation: - self.commands[self._create_entry()] = self.create() - if self.allow_deletion: - self.commands[self._delete_entry()] = self.delete() - self.extra_description = OrderedDict() - if extra_methods is not None: - for command in extra_methods: - self.commands[command[0].entry] = command[0] - self.extra_description[command[0].entry] = command[1] - - def description(self): - description = [' %s' % self.display_name, - ' %s : List %ss' % (self._list_entry(), self.entity_name), - ' %s : Get a %s by %s' % (self._get_entry(), self.entity_name, - 'UUID or name (first found then)' - if self.allow_retrieve_by_name - else 'UUID')] - if self.allow_creation: - description.append(' %s : Create a %s' % (self._create_entry(), self.entity_name)) - - if self.allow_deletion: - description.append(' %s : Delete a %s' % (self._delete_entry(), self.entity_name)) - description.extend([' %s : %s' % (k, v) for k, v in self.extra_description.items()]) - return description - - def generate_parser(self, parser): - for command in self.commands.values(): - command.generate_parser(parser) - - def is_handled(self, action): - return action in self.commands - - def execute(self, client, action, arguments): - return self.commands[action].execute(client, arguments) - - def _get_client_domain(self, client): - return getattr(getattr(client, self.api_version), self.client_domain) - - @staticmethod - def plural(entity_name): - if entity_name.endswith('y') and not (re.match(r'.+[aeiou]y', entity_name)): - return '%sies' % entity_name[:len(entity_name) - 1] - else: - return '%ss' % entity_name - - @staticmethod - def is_guid(s): - return re.match(r'[\d|a-z]{8}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{12}', s.lower()) is not None - - def id(self, entity): - if self.api_version == 'v2': - return entity['metadata']['guid'] - elif self.api_version == 'v3': - return entity['guid'] - - def resolve_id(self, argument, get_by_name): - if CommandDomain.is_guid(argument): - return argument - elif self.allow_retrieve_by_name: - result = get_by_name(argument) - if result is not None: - if self.api_version == 'v2': - return result['metadata']['guid'] - elif self.api_version == 'v3': - return result['guid'] - else: - raise InvalidStatusCode(NOT_FOUND, '%s with name %s' % (self.client_domain, argument)) - else: - raise ValueError('id: %s: does not allow search by name' % self.client_domain) - - def name(self, entity): - if self.api_version == 'v2': - return entity['entity'][self.name_property] - elif self.api_version == 'v3': - return entity[self.name_property] - - def find_by_name(self, client, name): - return self._get_client_domain(client).get_first(**{self.name_property: name}) - - def create(self): - entry = self._create_entry() - - def execute(client, arguments): - data = None - if os.path.isfile(arguments.entity[0]): - with open(arguments.entity[0], 'r') as f: - try: - data = json.load(f) - except ValueError: - raise ValueError('entity: file %s does not contain valid json data' % arguments.entity[0]) - else: - try: - data = json.loads(arguments.entity[0]) - except ValueError: - raise ValueError('entity: must be either a valid json file path or a json object') - print(self._get_client_domain(client)._create(data).json()) - - def generate_parser(parser): - create_parser = parser.add_parser(entry) - create_parser.add_argument('entity', metavar='entities', type=str, nargs=1, - help='Either a path of the json file containing the %s or a json object or the json %s object' % ( - self.client_domain, self.client_domain)) - - return Command(entry, generate_parser, execute) - - def delete(self): - entry = self._delete_entry() - - def execute(client, arguments): - if self.is_guid(arguments.id[0]): - self._get_client_domain(client)._remove(arguments.id[0]) - elif self.allow_retrieve_by_name: - entity = self.find_by_name(client, arguments.id[0]) - if entity is None: - raise InvalidStatusCode(NOT_FOUND, '%s with name %s' % (self.client_domain, arguments.id[0])) - else: - self._get_client_domain(client)._remove(self.id(entity)) - else: - raise ValueError('id: %s: does not allow search by name' % self.client_domain) - - def generate_parser(parser): - delete_parser = parser.add_parser(entry) - delete_parser.add_argument('id', metavar='ids', type=str, nargs=1, - help='The id. Can be UUID or name (first found then)' - if self.allow_retrieve_by_name else 'The id (UUID)') - - return Command(entry, generate_parser, execute) - - def get(self): - entry = self._get_entry() - - def execute(client, arguments): - resource_id = self.resolve_id(arguments.id[0], - functools.partial(self.find_by_name, client)) - print(self._get_client_domain(client).get(resource_id).json(indent=1)) - - def generate_parser(parser): - get_parser = parser.add_parser(entry) - get_parser.add_argument('id', metavar='ids', type=str, nargs=1, - help='The id. Can be UUID or name (first found then)' - if self.allow_retrieve_by_name else 'The id (UUID)') - - return Command(entry, generate_parser, execute) - - def list(self): - entry = self._list_entry() - - def execute(client, arguments): - filter_list = dict() - for filter_parameter in self.filter_list_parameters: - filter_value = getattr(arguments, filter_parameter) - if filter_value is not None: - filter_list[filter_parameter] = filter_value - for entity in self._get_client_domain(client).list(**filter_list): - if self.name_property is not None: - print('%s - %s' % (self.id(entity), self.name(entity))) - else: - print(self.id(entity)) - - def generate_parser(parser): - list_parser = parser.add_parser(entry) - for filter_parameter in self.filter_list_parameters: - list_parser.add_argument('-%s' % filter_parameter, action='store', dest=filter_parameter, type=str, - default=None, help='Filter with %s' % filter_parameter) - - return Command(entry, generate_parser, execute) - - def _list_entry(self): - return 'list_%s' % self.plural(self.entity_name) - - def _create_entry(self): - return 'create_%s' % self.entity_name - - def _delete_entry(self): - return 'delete_%s' % self.entity_name - - def _get_entry(self): - return 'get_%s' % self.entity_name diff --git a/main/cloudfoundry_client/main/main.py b/main/cloudfoundry_client/main/main.py deleted file mode 100644 index b9cb3f7..0000000 --- a/main/cloudfoundry_client/main/main.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/python -import argparse -import json -import logging -import os -import re -import sys - -from requests.exceptions import ConnectionError - -from cloudfoundry_client import __version__ -from cloudfoundry_client.client import CloudFoundryClient -from cloudfoundry_client.errors import InvalidStatusCode -from cloudfoundry_client.imported import NOT_FOUND -from cloudfoundry_client.main.apps_command_domain import AppCommandDomain -from cloudfoundry_client.main.command_domain import CommandDomain, Command -from cloudfoundry_client.main.operation_commands import generate_push_command -from cloudfoundry_client.main.tasks_command_domain import TaskCommandDomain - -__all__ = ['main', 'build_client_from_configuration'] - -_logger = logging.getLogger(__name__) - - -def _read_value_from_user(prompt, error_message=None, validator=None, default=''): - while True: - sys.stdout.write('%s [%s]: ' % (prompt, default)) - sys.stdout.flush() - answer_value = sys.stdin.readline().rstrip(' \r\n') - if len(answer_value) == 0: - answer_value = default - if len(answer_value) > 0 and (validator is None or validator(answer_value)): - return answer_value - else: - if error_message is None: - sys.stderr.write('\"%s\": invalid value\n' % answer_value) - else: - sys.stderr.write('\"%s\": %s\n' % (answer_value, error_message)) - - -def get_user_directory(): - dir_conf = os.path.join(os.path.expanduser('~')) - if not os.path.isdir(dir_conf): - if os.path.exists(dir_conf): - raise IOError('%s exists but is not a directory') - os.mkdir(dir_conf) - return dir_conf - - -def get_config_file(): - return os.path.join(get_user_directory(), '.cf_client_python.json') - - -def import_from_clf_cli(): - user_directory = get_user_directory() - cf_cli_dir = os.path.join(user_directory, '.cf') - if not os.path.isdir(cf_cli_dir): - raise IOError('%s directory not found' % cf_cli_dir) - config_file = os.path.join(cf_cli_dir, 'config.json') - if not os.path.isfile(config_file): - raise IOError('%s not found' % config_file) - with open(config_file, 'r') as cf_cli_file: - cf_cli_data = json.load(cf_cli_file) - if cf_cli_data['RefreshToken'] is None or cf_cli_data['Target'] is None: - raise IOError('Could not load informations from cf cli configuration') - with open(get_config_file(), 'w') as f: - f.write(json.dumps(dict(target_endpoint=cf_cli_data['Target'], - verify=False, - refresh_token=cf_cli_data['RefreshToken']), indent=2)) - - -def build_client_from_configuration(previous_configuration=None): - config_file = get_config_file() - if not os.path.isfile(config_file): - target_endpoint = _read_value_from_user('Please enter a target endpoint', - 'Url must starts with http:// or https://', - lambda s: s.startswith('http://') or s.startswith('https://'), - default='' if previous_configuration is None else - previous_configuration.get('target_endpoint', '')) - verify = _read_value_from_user('Verify ssl (true/false)', - 'Enter either true or false', - lambda s: s == 'true' or s == 'false', - default='true' if previous_configuration is None else - json.dumps( - previous_configuration.get('verify', True))) - login = _read_value_from_user('Please enter your login') - password = _read_value_from_user('Please enter your password') - client = CloudFoundryClient(target_endpoint, verify=(verify == 'true')) - client.init_with_user_credentials(login, password) - with open(config_file, 'w') as f: - f.write(json.dumps(dict(target_endpoint=target_endpoint, - verify=(verify == 'true'), - refresh_token=client.refresh_token), indent=2)) - return client - else: - try: - configuration = None - with open(config_file, 'r') as f: - configuration = json.load(f) - client = CloudFoundryClient(configuration['target_endpoint'], - verify=configuration['verify']) - client.init_with_token(configuration['refresh_token']) - return client - except Exception as ex: - if type(ex) == ConnectionError: - raise - else: - _logger.exception("Could not restore configuration. Cleaning and recreating") - os.remove(config_file) - return build_client_from_configuration(configuration) - - -def is_guid(s): - return re.match(r'[\d|a-z]{8}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{4}-[\d|a-z]{12}', s.lower()) is not None - - -def resolve_id(argument, get_by_name, domain_name, allow_search_by_name): - if is_guid(argument): - return argument - elif allow_search_by_name: - result = get_by_name(argument) - if result is not None: - return result['metadata']['guid'] - else: - raise InvalidStatusCode(NOT_FOUND, '%s with name %s' % (domain_name, argument)) - else: - raise ValueError('id: %s: does not allow search by name' % domain_name) - - -def log_recent(client, application_guid): - for envelope in client.doppler.recent_logs(application_guid): - _logger.info(envelope) - - -def stream_logs(client, application_guid): - try: - for envelope in client.doppler.stream_logs(application_guid): - _logger.info(envelope) - except KeyboardInterrupt: - pass - - -def _get_v2_client_domain(client, domain): - return getattr(client.v2, '%ss' % domain) - - -def generate_oauth_token_command(): - entry = 'oauth-token' - - def generate_parser(parser): - parser.add_parser(entry) - - def execute(client, arguments): - token = client._access_token - print(token if token is not None else 'No token') - - return Command(entry, generate_parser, execute), 'Display oauth token' - - -def main(): - logging.basicConfig(level=logging.INFO, - format='%(message)s') - logging.getLogger("requests").setLevel(logging.WARNING) - logging.getLogger("urllib3").setLevel(logging.WARNING) - - commands = [ - CommandDomain(display_name='Organizations', entity_name='organization', filter_list_parameters=[], - allow_retrieve_by_name=True, allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Spaces', entity_name='space', filter_list_parameters=['organization_guid'], - allow_retrieve_by_name=True, allow_creation=True, allow_deletion=True), - AppCommandDomain(), - CommandDomain(display_name='Services', entity_name='service', filter_list_parameters=['service_broker_guid'], - name_property='label', allow_retrieve_by_name=True, allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Service Plans', entity_name='service_plan', - filter_list_parameters=['service_guid', 'service_instance_guid', 'service_broker_guid']), - CommandDomain(display_name='Service Instances', entity_name='service_instance', - filter_list_parameters=['organization_guid', 'space_guid', 'service_plan_guid'], - allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Service Keys', entity_name='service_key', - filter_list_parameters=['service_instance_guid'], - allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Service Bindings', entity_name='service_binding', - filter_list_parameters=['app_guid', 'service_instance_guid'], name_property=None, - allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Service Broker', entity_name='service_broker', - filter_list_parameters=['name', 'space_guid'], - allow_retrieve_by_name=True, allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Service Plan Visibilities', entity_name='service_plan_visibility', - filter_list_parameters=['organization_guid', 'service_plan_guid'], name_property=None, - allow_retrieve_by_name=False, allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Buildpacks', entity_name='buildpack', - api_version='v3', - filter_list_parameters=[], allow_retrieve_by_name=True, - allow_creation=True, allow_deletion=True), - CommandDomain(display_name='Routes', entity_name='route', name_property='host', filter_list_parameters=[]), - TaskCommandDomain() - ] - operation_commands = [generate_push_command()] - others_commands = [generate_oauth_token_command()] - - descriptions = [] - for command in commands: - descriptions.extend(command.description()) - - descriptions.append('Operations') - for command, description in operation_commands: - descriptions.append(' %s: %s' % (command.entry, description)) - - descriptions.append('Others') - for command, description in others_commands: - descriptions.append(' %s: %s' % (command.entry, description)) - - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument('-V', '--version', action='version', version=__version__) - subparsers = parser.add_subparsers(title='Commands', dest='action', description='\n'.join(descriptions)) - subparsers.add_parser('import_from_cf_cli', help='Copy CF CLI configuration into our configuration') - - for command in commands: - command.generate_parser(subparsers) - for other_command_domain in [operation_commands, others_commands]: - for command, _ in other_command_domain: - command.generate_parser(subparsers) - - arguments = parser.parse_args() - if arguments.action == 'import_from_cf_cli': - import_from_clf_cli() - else: - client = build_client_from_configuration() - for command in commands: - if command.is_handled(arguments.action): - command.execute(client, arguments.action, arguments) - return - for other_command_domain in [operation_commands, others_commands]: - for command, _ in other_command_domain: - if command.entry == arguments.action: - command.execute(client, arguments) - return - - raise ValueError("Domain not found for action %s" % arguments.action) - - -if __name__ == "__main__": - main() diff --git a/main/cloudfoundry_client/main/operation_commands.py b/main/cloudfoundry_client/main/operation_commands.py deleted file mode 100644 index 6e39022..0000000 --- a/main/cloudfoundry_client/main/operation_commands.py +++ /dev/null @@ -1,19 +0,0 @@ -from cloudfoundry_client.main.command_domain import Command -from cloudfoundry_client.operations.push.push import PushOperation - - -def generate_push_command(): - entry = 'push_app' - - def generate_parser(parser): - command_parser = parser.add_parser(entry) - command_parser.add_argument('manifest_path', metavar='manifest_paths', type=str, nargs=1, - help='The manifest path') - command_parser.add_argument('-space_guid', action='store', dest='space_guid', type=str, - help='Space guid') - - def execute(client, arguments): - manifest_path = arguments.manifest_path[0] - PushOperation(client).push(arguments.space_guid, manifest_path) - - return Command(entry, generate_parser, execute), 'Push an application by its manifest' diff --git a/main/cloudfoundry_client/main/tasks_command_domain.py b/main/cloudfoundry_client/main/tasks_command_domain.py deleted file mode 100644 index f5311d8..0000000 --- a/main/cloudfoundry_client/main/tasks_command_domain.py +++ /dev/null @@ -1,64 +0,0 @@ -import json -import os - -from cloudfoundry_client.main.command_domain import CommandDomain, Command - - -class TaskCommandDomain(CommandDomain): - def __init__(self): - super(TaskCommandDomain, self).__init__(display_name='Tasks', entity_name='task', - filter_list_parameters=['names', 'app_guids', 'space_guids', - 'organization_guids'], - api_version='v3', allow_creation=True, allow_deletion=False, - extra_methods=[(self.cancel(), 'Cancel Task',)]) - - @staticmethod - def id(entity): - return entity['guid'] - - def name(self, entity): - return entity[self.name_property] - - def find_by_name(self, client, name): - return self._get_client_domain(client).get_first(**{'%ss' % self.name_property: name}) - - def create(self): - entry = self._create_entry() - - def execute(client, arguments): - data = None - if os.path.isfile(arguments.entity[0]): - with open(arguments.entity[0], 'r') as f: - try: - data = json.load(f) - except ValueError: - raise ValueError('entity: file %s does not contain valid json data' % arguments.entity[0]) - else: - try: - data = json.loads(arguments.entity[0]) - except ValueError: - raise ValueError('entity: must be either a valid json file path or a json object') - print(self._get_client_domain(client).create(arguments.app_id[0], **data).json()) - - def generate_parser(parser): - create_parser = parser.add_parser(entry) - create_parser.add_argument('app_id', metavar='ids', type=str, nargs=1, - help='The application UUID.') - create_parser.add_argument('entity', metavar='entities', type=str, nargs=1, - help='Either a path of the json file containing the %s or a json object or the json %s object' % ( - self.client_domain, self.client_domain)) - - return Command(entry, generate_parser, execute) - - def cancel(self): - entry = 'cancel_task' - - def execute(client, arguments): - print(self._get_client_domain(client).cancel(arguments.id[0]).json(indent=1)) - - def generate_parser(parser): - command_parser = parser.add_parser(entry) - command_parser.add_argument('id', metavar='ids', type=str, nargs=1, - help='The task UUID') - - return Command(entry, generate_parser, execute) diff --git a/main/cloudfoundry_client/operations/push/cf_ignore.py b/main/cloudfoundry_client/operations/push/cf_ignore.py deleted file mode 100644 index 62d353c..0000000 --- a/main/cloudfoundry_client/operations/push/cf_ignore.py +++ /dev/null @@ -1,38 +0,0 @@ -import fnmatch -import logging -import os - -_logger = logging.getLogger(__name__) - - -class CfIgnore(object): - def __init__(self, application_path): - ignore_file_path = os.path.join(application_path, '.cfignore') - self.ignore_items = [] - if os.path.isfile(ignore_file_path): - with open(ignore_file_path, 'r') as ignore_file: - for line in ignore_file.readlines(): - self.ignore_items.extend(self._pattern(line.strip(' \n'))) - - def is_entry_ignored(self, relative_file): - def is_relative_file_ignored(cf_ignore_entry): - _logger.debug('is_relative_file_ignored - %s - %s', cf_ignore_entry, relative_file) - file_path = '/%s' % relative_file \ - if cf_ignore_entry.startswith('/') and not relative_file.startswith('/') \ - else relative_file - return fnmatch.fnmatch(file_path, cf_ignore_entry) - - return any([is_relative_file_ignored(ignore_item) - for ignore_item in self.ignore_items]) - - @staticmethod - def _pattern(pattern): - if pattern.find('/') < 0: - return [pattern, os.path.join('**', pattern)] - elif pattern.endswith('/'): - return [os.path.join('/', pattern, '*'), - os.path.join('/', pattern, '**', '*'), - os.path.join('/', '**', pattern, '*'), - os.path.join('/', '**', pattern, '**', '*')] - else: - return [os.path.join('/', pattern), os.path.join('/', '**', pattern)] diff --git a/main/cloudfoundry_client/operations/push/push.py b/main/cloudfoundry_client/operations/push/push.py deleted file mode 100644 index 770a815..0000000 --- a/main/cloudfoundry_client/operations/push/push.py +++ /dev/null @@ -1,341 +0,0 @@ -import json -import logging -import os -import re -import shutil -import tempfile -import time - -from cloudfoundry_client.operations.push.cf_ignore import CfIgnore -from cloudfoundry_client.operations.push.file_helper import FileHelper -from cloudfoundry_client.operations.push.validation.manifest import ManifestReader - -_logger = logging.getLogger(__name__) - - -class PushOperation(object): - UPLOAD_TIMEOUT = 15 * 60 - - SPLIT_ROUTE_PATTERN = re.compile('(?P[a-z]+://)?(?P[^:/]+)(?P:\d+)?(?P/.*)?') - - def __init__(self, client): - self.client = client - - def push(self, space_id, manifest_path, restart=True): - app_manifests = ManifestReader.load_application_manifests(manifest_path) - organization, space = self._retrieve_space_and_organization(space_id) - - for app_manifest in app_manifests: - if 'path' in app_manifest or 'docker' in app_manifest: - self._push_application(organization, space, app_manifest, restart) - - def _retrieve_space_and_organization(self, space_id): - space = self.client.v2.spaces.get(space_id) - organization = space.organization() - return organization, space - - def _push_application(self, organization, space, app_manifest, restart): - app = self._init_application(space, app_manifest) - self._route_application(organization, space, app, app_manifest.get('no-route', False), - app_manifest.get('routes', []), app_manifest.get('random-route', False)) - if 'path' in app_manifest: - self._upload_application(app, app_manifest['path']) - self._bind_services(space, app, app_manifest.get('services', [])) - if restart: - PushOperation._restart_application(app) - - def _init_application(self, space, app_manifest): - app = self.client.v2.apps.get_first(name=app_manifest['name'], space_guid=space['metadata']['guid']) - return self._update_application(app, app_manifest) if app is not None \ - else self._create_application(space, app_manifest) - - def _create_application(self, space, app_manifest): - _logger.debug("Creating application %s", app_manifest['name']) - request = self._build_request_from_manifest(app_manifest) - request['environment_json'] = PushOperation._merge_environment(None, app_manifest) - request['space_guid'] = space['metadata']['guid'] - if request.get('health-check-type') == 'http' and request.get('health-check-http-endpoint') is None: - request['health-check-http-endpoint'] = '/' - return self.client.v2.apps.create(**request) - - def _update_application(self, app, app_manifest): - _logger.debug("Uploading application %s", app['entity']['name']) - request = self._build_request_from_manifest(app_manifest) - request['environment_json'] = PushOperation._merge_environment(app, app_manifest) - if request.get('health-check-type') == 'http' and request.get('health-check-http-endpoint') is None \ - and app['entity'].get('health_check_http_endpoint') is None: - request['health-check-http-endpoint'] = '/' - return self.client.v2.apps.update(app['metadata']['guid'], **request) - - def _build_request_from_manifest(self, app_manifest): - request = dict() - request.update(app_manifest) - stack = self.client.v2.stacks.get_first(name=app_manifest['stack']) if 'stack' in app_manifest else None - if stack is not None: - request['stack_guid'] = stack['metadata']['guid'] - docker = request.pop('docker', None) - if docker is not None and 'image' in docker: - request['docker_image'] = docker['image'] - request['diego'] = True - if 'username' in docker and 'password' in docker: - request['docker_credentials'] = dict(username=docker['username'], password=docker['password']) - buildpacks = request.pop('buildpacks', None) - if 'buildpack' not in request and buildpacks is not None and len(buildpacks) > 0: - request['buildpack'] = buildpacks[0] - return request - - @staticmethod - def _merge_environment(app, app_manifest): - environment = dict() - if app is not None and 'environment_json' in app['entity']: - environment.update(app['entity']['environment_json']) - if 'env' in app_manifest: - environment.update(app_manifest['env']) - return environment - - def _route_application(self, organization, space, app, no_route, routes, random_route): - existing_routes = [route for route in app.routes()] - if no_route: - self._remove_all_routes(app, existing_routes) - elif len(routes) == 0 and len(existing_routes) == 0: - self._build_default_route(space, app, random_route) - else: - self._build_new_requested_routes(organization, space, app, existing_routes, routes) - - def _remove_all_routes(self, app, routes): - for route in routes: - self.client.v2.apps.remove_route(app['metadata']['guid'], route['metadata']['guid']) - - def _build_default_route(self, space, app, random_route): - shared_domain = None - for domain in self.client.v2.shared_domains.list(): - if not domain['entity'].get('internal', False): - shared_domain = domain - break - if shared_domain is None: - raise AssertionError('No route specified and no no-route field or shared domain') - if shared_domain['entity'].get('router_group_type') == 'tcp': - route = self.client.v2.routes.create_tcp_route(shared_domain['metadata']['guid'], - space['metadata']['guid']) - elif random_route: - route = self.client.v2.routes.create_host_route(shared_domain['metadata']['guid'], - space['metadata']['guid'], - self._to_host( - '%s-%d' % (app['entity']['name'], int(time.time())))) - else: - route = self.client.v2.routes.create_host_route(shared_domain['metadata']['guid'], - space['metadata']['guid'], - self._to_host(app['entity']['name'])) - self.client.v2.apps.associate_route(app['metadata']['guid'], route['metadata']['guid']) - - def _build_new_requested_routes(self, organization, space, app, existing_routes, requested_routes): - private_domains = {domain['entity']['name']: domain for domain in organization.private_domains()} - shared_domains = {domain['entity']['name']: domain for domain in self.client.v2.shared_domains.list()} - for requested_route in requested_routes: - route, port, path = PushOperation._split_route(requested_route) - if len(path) > 0 and port is not None: - _logger.error("Neither path nor port provided for route", requested_route) - raise AssertionError('Cannot set both port and path for route: %s' % requested_route) - host, domain_name, domain = PushOperation._resolve_domain(route, private_domains, shared_domains) - if port is not None and host is not None: - _logger.error('Host provided in route %s for tcp domain %s', requested_route, domain_name) - raise AssertionError( - 'For route (%s) refers to domain %s that is a tcp one. It is hence routed by port and not by host' - % (requested_route, domain_name)) - route_to_map = None - if port is not None and domain['entity'].get('router_group_type') != 'tcp': - _logger.error('Port provided in route %s for non tcp domain %s', requested_route, domain_name) - raise AssertionError('Cannot set port on route(%s) for non tcp domain' % requested_route) - elif domain['entity'].get('router_group_type') == 'tcp' and port is None: - _logger.error('No port provided in route %s for tcp domain %s', requested_route, domain_name) - raise AssertionError('Please specify a port on route (%s) for tcp domain' % requested_route) - elif domain['entity'].get('router_group_type') == 'tcp': - if not any([route['entity']['domain_guid'] == domain['metadata']['guid'] - and route['entity']['port'] == port] for route in existing_routes): - route_to_map = self._resolve_new_tcp_route(space, domain, port) - else: - if not any([route['entity']['domain_guid'] == domain['metadata']['guid'] - and route['entity']['host'] == host] for route in existing_routes): - route_to_map = self._resolve_new_host_route(space, domain, host, path) - if route_to_map is not None: - _logger.debug('Associating route %s to application %s', requested_route, app['entity']['name']) - self.client.v2.apps.associate_route(app['metadata']['guid'], route_to_map['metadata']['guid']) - - def _resolve_new_host_route(self, space, domain, host, path): - existing_route = self.client.v2.routes.get_first(domain_guid=domain['metadata']['guid'], host=host, path=path) - if existing_route is None: - _logger.debug('Creating host route %s on domain %s and path %s', host, domain['entity']['name'], path) - existing_route = self.client.v2.routes.create_host_route(domain['metadata']['guid'], - space['metadata']['guid'], - host, - path) - else: - _logger.debug('Host route %s on domain %s and path %s already exists with guid %s', - host, - domain['entity']['name'], - path, - existing_route['metadata']['guid']) - return existing_route - - def _resolve_new_tcp_route(self, space, domain, port): - existing_route = self.client.v2.routes.get_first(domain_guid=domain['metadata']['guid'], port=port) - if existing_route is None: - _logger.debug('Creating tcp route %d on domain %s', port, domain['entity']['name']) - existing_route = self.client.v2.routes.create_tcp_route(domain['metadata']['guid'], - space['metadata']['guid'], - port) - else: - _logger.debug('TCP route %d on domain %s already exists with guid %s', - port, - domain['entity']['name'], - existing_route['metadata']['guid']) - return existing_route - - @staticmethod - def _split_route(requested_route): - route_splitted = PushOperation.SPLIT_ROUTE_PATTERN.match(requested_route['route']) - if route_splitted is None: - raise AssertionError('Invalid route: %s' % requested_route['route']) - domain = route_splitted.group('domain') - port = route_splitted.group('port') - path = route_splitted.group('path') - return domain, int(port[1:]) if port is not None else None, '' if path is None or path == '/' else path - - @staticmethod - def _resolve_domain(route, private_domains, shared_domains): - for domains in [private_domains, shared_domains]: - if route in domains: - return '', route, domains[route] - else: - idx = route.find('.') - if 0 < idx < (len(route) - 2): - host = route[:idx] - domain = route[idx + 1:] - if domain in domains: - return host, domain, domains[domain] - raise AssertionError('Cannot find domain for route %s' % route) - - def _upload_application(self, app, application_path): - _logger.debug('Uploading application %s', app['entity']['name']) - if os.path.isfile(application_path): - self._upload_application_zip(app, application_path) - elif os.path.isdir(application_path): - self._upload_application_directory(app, application_path) - else: - raise AssertionError('Path %s is neither a directory nor a file' % application_path) - - def _upload_application_zip(self, app, path): - _logger.debug('Unzipping file %s', path) - tmp_dir = tempfile.mkdtemp() - try: - FileHelper.unzip(path, tmp_dir) - self._upload_application_directory(app, tmp_dir) - finally: - shutil.rmtree(tmp_dir) - - def _upload_application_directory(self, app, application_path): - _logger.debug('Uploading application from directory %s', application_path) - _, temp_file = tempfile.mkstemp() - try: - resource_descriptions_by_path = PushOperation._load_all_resources(application_path) - - def generate_key(item): - return '%s-%d' % (item["sha1"], item["size"]) - - already_uploaded_entries = [generate_key(item) for item in - self.client.v2.resources.match([dict(sha1=item["sha1"], size=item["size"]) - for item in - resource_descriptions_by_path.values()])] - _logger.debug('Already uploaded %d / %d items', - len(already_uploaded_entries), len(resource_descriptions_by_path)) - - FileHelper.zip(temp_file, application_path, - lambda item: item in resource_descriptions_by_path - and generate_key(resource_descriptions_by_path[item]) not in already_uploaded_entries) - _logger.debug('Diff zip file built: %s', temp_file) - resources = [ - dict(fn=resource_path, - sha1=resource_description["sha1"], - size=resource_description["size"], - mode=resource_description["mode"]) - for resource_path, resource_description in resource_descriptions_by_path.items() - if generate_key(resource_description) in already_uploaded_entries - ] - _logger.debug('Uploading bits of application') - job = self.client.v2.apps.upload(app['metadata']['guid'], - resources, - temp_file, - True) - self._poll_job(job) - finally: - _logger.debug('Skipping remove of zip file') - - @staticmethod - def _load_all_resources(top_directory): - application_items = {} - cf_ignore = CfIgnore(top_directory) - for directory, file_names in FileHelper.walk(top_directory): - for file_name in file_names: - relative_file_location = os.path.join(directory, file_name) - if not cf_ignore.is_entry_ignored(relative_file_location): - absolute_file_location = os.path.join(top_directory, relative_file_location) - application_items[relative_file_location] = dict( - sha1=FileHelper.sha1(absolute_file_location), - size=FileHelper.size(absolute_file_location), - mode=FileHelper.mode(absolute_file_location)) - return application_items - - def _bind_services(self, space, app, services): - service_instances = [service_instance for service_instance in space.service_instances( - return_user_provided_service_instances="true")] - service_name_to_instance_guid = {service_instance["entity"]["name"]: service_instance["metadata"]["guid"] - for service_instance in service_instances} - existing_service_instance_guid = [service_binding['entity']['service_instance_guid'] - for service_binding in app.service_bindings()] - for service_name in services: - service_instance_guid = service_name_to_instance_guid.get(service_name) - if service_instance_guid is None: - raise AssertionError('No service found with name %s' % service_name) - elif service_instance_guid in existing_service_instance_guid: - _logger.debug('%s already bound to %s', app["entity"]["name"], service_name) - else: - _logger.debug('Binding %s to %s', app["entity"]["name"], service_name) - self.client.v2.service_bindings.create(app['metadata']['guid'], service_instance_guid) - - def _poll_job(self, job): - def job_not_ended(j): - return j['entity']['status'] in ['queued', 'running'] - - job_guid = job['metadata']['guid'] - _logger.debug('Waiting for upload of application to be complete. Polling job %s...', job_guid) - started_time = time.time() - elapsed_time = 0 - - while job_not_ended(job) and elapsed_time < PushOperation.UPLOAD_TIMEOUT: - _logger.debug('Getting job status %s..', job_guid) - job = self.client.v2.jobs.get(job_guid) - if job_not_ended(job): - time.sleep(5) - elapsed_time = int(time.time() - started_time) - if job_not_ended(job): - raise AssertionError('Exceeded timeout while polling job of upload') - elif job['entity']['status'] == 'failed': - raise AssertionError('Job of upload exceeded in error: %s', json.dumps(job['entity']['error_details'])) - else: - _logger.debug('Job ended with status %s', job['entity']['status']) - - @staticmethod - def _restart_application(app): - _logger.debug("Restarting application") - app.stop() - app.start() - - @staticmethod - def _to_host(host): - def no_space(h): - return re.sub('[\s_]+', "-", h) - - def only_alphabetical_and_hyphen(h): - return re.sub("[^a-z0-9-]", "", h) - - return only_alphabetical_and_hyphen(no_space(host)) diff --git a/main/cloudfoundry_client/operations/push/validation/manifest.py b/main/cloudfoundry_client/operations/push/validation/manifest.py deleted file mode 100644 index b074c2c..0000000 --- a/main/cloudfoundry_client/operations/push/validation/manifest.py +++ /dev/null @@ -1,131 +0,0 @@ -import json -import os -import re - -import yaml - - -class ManifestReader(object): - MEMORY_PATTERN = re.compile("^(\d+)([KMGT])B?$") - - POSITIVE_FIELDS = ['instances', 'timeout'] - - BOOLEAN_FIELDS = ['no-route', 'random-route'] - - @staticmethod - def load_application_manifests(manifest_path): - with open(manifest_path, 'r') as fp: - manifest = yaml.safe_load(fp) - if manifest is None: - raise AssertionError('No valid yaml document found') - ManifestReader._validate_manifest(os.path.dirname(manifest_path), manifest) - return manifest['applications'] - - @staticmethod - def _validate_manifest(manifest_directory, manifest): - for app_manifest in manifest['applications']: - ManifestReader._validate_application_manifest(manifest_directory, app_manifest) - - @staticmethod - def _validate_application_manifest(manifest_directory, app_manifest): - name = app_manifest.get('name') - if name is None or len(name) == 0: - raise AssertionError('name must be set') - docker_manifest = app_manifest.get('docker') - if docker_manifest is not None: - if app_manifest.get('path') is not None: - raise AssertionError('Both path and docker cannot be set') - ManifestReader._validate_application_docker(docker_manifest) - elif 'path' not in app_manifest: - raise AssertionError('One of path or docker must be set') - else: - ManifestReader._absolute_path(manifest_directory, app_manifest) - ManifestReader._convert_memory(app_manifest) - for field in ManifestReader.POSITIVE_FIELDS: - ManifestReader._convert_positive(app_manifest, field) - for field in ManifestReader.BOOLEAN_FIELDS: - ManifestReader._convert_boolean(app_manifest, field) - ManifestReader._convert_environment(app_manifest) - ManifestReader._check_deprecated_attributes(app_manifest) - ManifestReader._validate_routes(app_manifest) - - @staticmethod - def _check_deprecated_attributes(app_manifest): - if app_manifest.get('hosts') is not None or app_manifest.get('host') \ - or app_manifest.get('domains') is not None or app_manifest.get('domain') \ - or app_manifest.get('no-hostname') is not None: - raise AssertionError( - 'hosts, host, domains, domain and no-hostname are all deprecated. Use the routes attribute') - - @staticmethod - def _convert_memory(manifest): - if 'memory' in manifest: - memory = manifest['memory'].upper() - match = ManifestReader.MEMORY_PATTERN.match(memory) - if match is None: - raise AssertionError("Invalid memory format: %s" % memory) - - memory_number = int(match.group(1)) - if match.group(2) == 'K': - memory_number *= 1024 - elif match.group(2) == 'M': - memory_number *= 1024 * 1024 - elif match.group(2) == 'G': - memory_number *= 1024 * 1024 * 1024 - elif match.group(2) == 'T': - memory_number *= 1024 * 1024 * 1024 * 1024 - else: - raise AssertionError('Invalid memory unit: %s' % memory) - manifest['memory'] = int(memory_number / (1024 * 1024)) - - @staticmethod - def _convert_positive(manifest, field): - if field in manifest: - value = int(manifest[field]) - if value < 1: - raise AssertionError("Invalid %s value: %s. It ust be positive" % (field, s)) - manifest[field] = value - - @staticmethod - def _convert_boolean(manifest, field): - if field in manifest: - field_value = manifest[field] - manifest[field] = field_value if type(field_value) == bool else field_value.lower() == 'true' - - @staticmethod - def _validate_routes(manifest): - for route in manifest.get('routes', []): - if type(route) != dict or 'route' not in route: - raise AssertionError('routes attribute must be a list of object containing a route attribute') - - @staticmethod - def _validate_application_docker(docker_manifest): - docker_image = docker_manifest.get('image') - if docker_image is not None and docker_manifest.get('buildpack') is not None: - raise AssertionError('image and buildpack can not both be set for docker') - docker_username = docker_manifest.get('username') - docker_password = docker_manifest.get('password') - if docker_username is not None and docker_password is None or docker_username is None and docker_password is not None: - raise AssertionError('Docker username/password must be set together or both be unset') - if docker_username is not None and docker_password is not None and docker_image is None: - raise AssertionError('Docker image not set while docker username/password are set') - - @staticmethod - def _absolute_path(manifest_directory, manifest): - if 'path' in manifest: - path = manifest['path'] - if path == os.path.abspath(path): - manifest['path'] = path - elif manifest_directory == '' or manifest_directory == '.': - manifest['path'] = os.path.abspath(path) - else: - manifest['path'] = os.path.abspath(os.path.join(manifest_directory, path)) - - @staticmethod - def _convert_environment(app_manifest): - environment = app_manifest.get('env', None) - if environment is not None: - if type(environment) != dict: - raise AssertionError("'env' entry must be a dictionary") - app_manifest['env'] = {key: json.dumps(value) for key, value in environment.items() if value is not None and type(value) != str} - app_manifest['env'].update({key: value for key, value in environment.items() if value is not None and type(value) == str}) diff --git a/main/cloudfoundry_client/request_object.py b/main/cloudfoundry_client/request_object.py deleted file mode 100644 index 19fc851..0000000 --- a/main/cloudfoundry_client/request_object.py +++ /dev/null @@ -1,4 +0,0 @@ -class Request(dict): - def __setitem__(self, key, value): - if value is not None: - super(Request, self).__setitem__(key, value) \ No newline at end of file diff --git a/main/cloudfoundry_client/v2/apps.py b/main/cloudfoundry_client/v2/apps.py deleted file mode 100644 index 9fea414..0000000 --- a/main/cloudfoundry_client/v2/apps.py +++ /dev/null @@ -1,161 +0,0 @@ -import json -import logging -import os -from time import sleep - -from cloudfoundry_client.errors import InvalidStatusCode -from cloudfoundry_client.imported import BAD_REQUEST -from cloudfoundry_client.json_object import JsonObject -from cloudfoundry_client.v2.entities import Entity, EntityManager - -_logger = logging.getLogger(__name__) - - -class _Application(Entity): - def instances(self): - return self.client.v2.apps.get_instances(self['metadata']['guid']) - - def start(self): - return self.client.v2.apps.start(self['metadata']['guid']) - - def stop(self): - return self.client.v2.apps.stop(self['metadata']['guid']) - - def stats(self): - return self.client.v2.apps.get_stats(self['metadata']['guid']) - - def env(self): - return self.client.v2.apps.get_env(self['metadata']['guid']) - - def summary(self): - return self.client.v2.apps.get_summary(self['metadata']['guid']) - - def restage(self): - return self.client.v2.apps.restage(self['metadata']['guid']) - - def recent_logs(self): - return self.client.doppler.recent_logs(self['metadata']['guid']) - - def stream_logs(self): - return self.client.doppler.stream_logs(self['metadata']['guid']) - - -class AppManager(EntityManager): - APPLICATION_FIELDS = ['name', 'memory', 'instances', 'disk_quota', 'space_guid', 'stack_guid', 'state', 'command', - 'buildpack', 'health_check_http_endpoint', 'health_check_type', 'health_check_timeout', - 'diego', 'enable_ssh', 'docker_image', 'docker_credentials', 'environment_json', 'production', - 'console', 'debug', 'staging_failed_reason', 'staging_failed_description', 'ports'] - - def __init__(self, target_endpoint, client): - super(AppManager, self).__init__(target_endpoint, client, '/v2/apps', - lambda pairs: _Application(target_endpoint, client, pairs)) - - def get_stats(self, application_guid): - return self._get('%s/%s/stats' % (self.entity_uri, application_guid), JsonObject) - - def get_instances(self, application_guid): - return self._get('%s/%s/instances' % (self.entity_uri, application_guid), JsonObject) - - def get_env(self, application_guid): - return self._get('%s/%s/env' % (self.entity_uri, application_guid), JsonObject) - - def get_summary(self, application_guid): - return self._get('%s/%s/summary' % (self.entity_uri, application_guid), JsonObject) - - def associate_route(self, application_guid, route_guid): - self._put('%s%s/%s/routes/%s' % (self.target_endpoint, self.entity_uri, application_guid, route_guid)) - - def list_routes(self, application_guid, **kwargs): - return self.client.v2.routes._list('%s/%s/routes' % (self.entity_uri, application_guid), **kwargs) - - def remove_route(self, application_guid, route_guid): - self._delete('%s%s/%s/routes/%s' % (self.target_endpoint, self.entity_uri, application_guid, route_guid)) - - def list_service_bindings(self, application_guid, **kwargs): - return self.client.v2.service_bindings._list('%s/%s/service_bindings' % (self.entity_uri, application_guid), - **kwargs) - - def start(self, application_guid, check_time=0.5, timeout=300, asynchronous=False): - result = super(AppManager, self)._update(application_guid, - dict(state='STARTED')) - if asynchronous: - return result - else: - summary = self.get_summary(application_guid) - self._wait_for_instances_in_state(application_guid, summary['instances'], 'RUNNING', check_time, timeout) - return result - - def stop(self, application_guid, check_time=0.5, timeout=500, asynchronous=False): - result = super(AppManager, self)._update(application_guid, dict(state='STOPPED')) - if asynchronous: - return result - else: - self._wait_for_instances_in_state(application_guid, 0, 'STOPPED', check_time, timeout) - return result - - def restage(self, application_guid): - return self._post("%s%s/%s/restage" % (self.target_endpoint, self.entity_uri, application_guid)) - - def create(self, **kwargs): - if kwargs.get('name') is None or kwargs.get('space_guid') is None: - raise AssertionError('Please provide a name and a space_guid') - request = AppManager._generate_application_update_request(**kwargs) - return super(AppManager, self)._create(request) - - def update(self, application_guid, **kwargs): - request = AppManager._generate_application_update_request(**kwargs) - return super(AppManager, self)._update(application_guid, request) - - def remove(self, application_guid): - super(AppManager, self)._remove(application_guid) - - def upload(self, application_guid, resources, application, asynchronous=False): - application_size = os.path.getsize(application) - with open(application, 'rb') as binary_file: - return self.client.put("%s%s/%s/bits" % (self.target_endpoint, self.entity_uri, application_guid), - params={"async": "true" if asynchronous else "false"} if asynchronous else None, - data=dict(resources=json.dumps(resources)), - files=dict(application=('application.zip', - binary_file, - 'application/zip', - {'Content-Length': application_size, - 'Content-Transfer-Encoding': 'binary'}))) \ - .json(object_pairs_hook=JsonObject) - - @staticmethod - def _generate_application_update_request(**kwargs): - return {key: kwargs[key] for key in AppManager.APPLICATION_FIELDS if key in kwargs} - - def _wait_for_instances_in_state(self, application_guid, number_required, state_expected, check_time, timeout): - all_in_expected_state = False - sum_waiting = 0 - while not all_in_expected_state: - instances = self._safe_get_instances(application_guid) - number_in_expected_state = 0 - for instance_number, instance in list(instances.items()): - if instance['state'] == state_expected: - number_in_expected_state += 1 - # this case will make this code work for both stop and start operation - all_in_expected_state = number_in_expected_state == number_required - if not all_in_expected_state: - _logger.debug('_wait_for_instances_in_state - %d/%d %s', number_in_expected_state, - number_required, - state_expected) - if sum_waiting > timeout: - raise AssertionError('Failed to get state %s for %d instances' % (state_expected, number_required)) - sleep(check_time) - sum_waiting += check_time - - def _safe_get_instances(self, application_guid): - try: - return self.get_instances(application_guid) - except InvalidStatusCode as ex: - if ex.status_code == BAD_REQUEST and type(ex.body) == dict: - code = ex.body.get('code', -1) - # 170002: staging not finished - # 220001: instances error - if code == 220001 or code == 170002: - return {} - else: - _logger.error("") - raise diff --git a/main/cloudfoundry_client/v2/buildpacks.py b/main/cloudfoundry_client/v2/buildpacks.py deleted file mode 100644 index 005896a..0000000 --- a/main/cloudfoundry_client/v2/buildpacks.py +++ /dev/null @@ -1,9 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class BuildpackManager(EntityManager): - def __init__(self, target_endpoint, client): - super(BuildpackManager, self).__init__(target_endpoint, client, '/v2/buildpacks') - - def update(self, buildpack_guid, parameters): - return super(BuildpackManager, self)._update(buildpack_guid, parameters) diff --git a/main/cloudfoundry_client/v2/entities.py b/main/cloudfoundry_client/v2/entities.py deleted file mode 100644 index 777411e..0000000 --- a/main/cloudfoundry_client/v2/entities.py +++ /dev/null @@ -1,162 +0,0 @@ -import functools -import logging - -from cloudfoundry_client.errors import InvalidEntity -from cloudfoundry_client.imported import quote, reduce -from cloudfoundry_client.json_object import JsonObject -from cloudfoundry_client.request_object import Request - -_logger = logging.getLogger(__name__) - - -class Entity(JsonObject): - def __init__(self, target_endpoint, client, *args, **kwargs): - super(Entity, self).__init__(*args, **kwargs) - self.target_endpoint = target_endpoint - self.client = client - try: - if 'entity' not in self: - raise InvalidEntity(**self) - - for attribute, value in list(self['entity'].items()): - domain_name, suffix = attribute.rpartition('_')[::2] - if suffix == 'url': - manager_name = domain_name if domain_name.endswith('s') else '%ss' % domain_name - try: - other_manager = getattr(client.v2, manager_name) - except AttributeError: - # generic manager - - other_manager = EntityManager( - target_endpoint, - client, - '') - if domain_name.endswith('s'): - new_method = functools.partial(other_manager._list, value) - else: - new_method = functools.partial(other_manager._get, value) - new_method.__name__ = domain_name - setattr(self, domain_name, new_method) - except KeyError: - raise InvalidEntity(**self) - - -class EntityManager(object): - list_query_parameters = ['page', 'results-per-page', 'order-direction'] - - list_multi_parameters = ['order-by'] - - def __init__(self, target_endpoint, client, entity_uri, entity_builder=None): - self.target_endpoint = target_endpoint - self.entity_uri = entity_uri - self.client = client - self.entity_builder = entity_builder if entity_builder is not None else lambda pairs: Entity(target_endpoint, - client, pairs) - - def _get(self, requested_path, entity_builder=None): - url = '%s%s' % (self.target_endpoint, requested_path) - response = self.client.get(url) - _logger.debug('GET - %s - %s', requested_path, response.text) - return self._read_response(response, entity_builder) - - def _list(self, requested_path, entity_builder=None, **kwargs): - url_requested = self._get_url_filtered('%s%s' % (self.target_endpoint, requested_path), **kwargs) - response = self.client.get(url_requested) - entity_builder = self._get_entity_builder(entity_builder) - while True: - _logger.debug('GET - %s - %s', url_requested, response.text) - response_json = self._read_response(response, JsonObject) - for resource in response_json['resources']: - yield entity_builder(list(resource.items())) - if response_json['next_url'] is None: - break - else: - url_requested = '%s%s' % (self.target_endpoint, response_json['next_url']) - response = self.client.get(url_requested) - - def _create(self, data, **kwargs): - url = '%s%s' % (self.target_endpoint, self.entity_uri) - return self._post(url, data, **kwargs) - - def _update(self, resource_id, data, **kwargs): - url = '%s%s/%s' % (self.target_endpoint, self.entity_uri, resource_id) - return self._put(url, data, **kwargs) - - def _remove(self, resource_id, **kwargs): - url = '%s%s/%s' % (self.target_endpoint, self.entity_uri, resource_id) - self._delete(url, **kwargs) - - def _post(self, url, data=None, **kwargs): - response = self.client.post(url, json=data, **kwargs) - _logger.debug('POST - %s - %s', url, response.text) - return self._read_response(response) - - def _put(self, url, data=None, **kwargs): - response = self.client.put(url, json=data, **kwargs) - _logger.debug('PUT - %s - %s', url, response.text) - return self._read_response(response) - - def _delete(self, url, **kwargs): - response = self.client.delete(url, **kwargs) - _logger.debug('DELETE - %s - %s', url, response.text) - - def __iter__(self): - return self.list() - - def __getitem__(self, entity_guid): - return self.get(entity_guid) - - def list(self, **kwargs): - return self._list(self.entity_uri, **kwargs) - - def get_first(self, **kwargs): - kwargs.setdefault('results-per-page', 1) - for entity in self._list(self.entity_uri, **kwargs): - return entity - return None - - def get(self, entity_id, *extra_paths): - if len(extra_paths) == 0: - requested_path = '%s/%s' % (self.entity_uri, entity_id) - else: - requested_path = '%s/%s/%s' % (self.entity_uri, entity_id, '/'.join(extra_paths)) - return self._get(requested_path) - - def _read_response(self, response, other_entity_builder=None): - entity_builder = self._get_entity_builder(other_entity_builder) - result = response.json(object_pairs_hook=JsonObject) - return entity_builder(list(result.items())) - - @staticmethod - def _request(**mandatory_parameters): - return Request(**mandatory_parameters) - - def _get_entity_builder(self, entity_builder): - if entity_builder is None: - return self.entity_builder - else: - return entity_builder - - def _get_url_filtered(self, url, **kwargs): - - def _append_encoded_parameter(parameters, args): - parameter_name, parameter_value = args[0], args[1] - if parameter_name in self.list_query_parameters: - parameters.append('%s=%s' % (parameter_name, str(parameter_value))) - elif parameter_name in self.list_multi_parameters: - value_list = parameter_value - if not isinstance(value_list, (list, tuple)): - value_list = [value_list] - 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)))) - else: - parameters.append('q=%s' % quote('%s:%s' % (parameter_name, str(parameter_value)))) - return parameters - - if len(kwargs) > 0: - return '%s?%s' % (url, - "&".join(reduce(_append_encoded_parameter, sorted(list(kwargs.items())), []))) - else: - return url diff --git a/main/cloudfoundry_client/v2/events.py b/main/cloudfoundry_client/v2/events.py deleted file mode 100644 index d47c6ee..0000000 --- a/main/cloudfoundry_client/v2/events.py +++ /dev/null @@ -1,9 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class EventManager(EntityManager): - def __init__(self, target_endpoint, client): - super(EventManager, self).__init__(target_endpoint, client, '/v2/events') - - def list_by_type(self, event_type): - return self._list(self.entity_uri, type=event_type) diff --git a/main/cloudfoundry_client/v2/jobs.py b/main/cloudfoundry_client/v2/jobs.py deleted file mode 100644 index f895270..0000000 --- a/main/cloudfoundry_client/v2/jobs.py +++ /dev/null @@ -1,10 +0,0 @@ -from cloudfoundry_client.json_object import JsonObject - - -class JobManager(object): - def __init__(self, target_endpoint, client): - self.target_endpoint = target_endpoint - self.client = client - - def get(self, job_guid): - return self.client.get('%s/v2/jobs/%s' % (self.target_endpoint, job_guid)).json(object_pairs_hook=JsonObject) diff --git a/main/cloudfoundry_client/v2/resources.py b/main/cloudfoundry_client/v2/resources.py deleted file mode 100644 index 83c8c37..0000000 --- a/main/cloudfoundry_client/v2/resources.py +++ /dev/null @@ -1,11 +0,0 @@ -from cloudfoundry_client.json_object import JsonObject - - -class ResourceManager(object): - def __init__(self, target_endpoint, client): - self.target_endpoint = target_endpoint - self.client = client - - def match(self, items): - response = self.client.put('%s/v2/resource_match' % self.client.info.api_endpoint, json=items) - return response.json(object_pairs_hook=JsonObject) diff --git a/main/cloudfoundry_client/v2/routes.py b/main/cloudfoundry_client/v2/routes.py deleted file mode 100644 index 7a3f506..0000000 --- a/main/cloudfoundry_client/v2/routes.py +++ /dev/null @@ -1,18 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class RouteManager(EntityManager): - def __init__(self, target_endpoint, client): - super(RouteManager, self).__init__(target_endpoint, client, '/v2/routes') - - def create_tcp_route(self, domain_guid, space_guid, port=None): - request = self._request(domain_guid=domain_guid, space_guid=space_guid) - if port is None: - return super(RouteManager, self)._create(request, params=dict(generate_port=True)) - else: - request['port'] = port - return super(RouteManager, self)._create(request) - - def create_host_route(self, domain_guid, space_guid, host, path=''): - request = dict(domain_guid=domain_guid, space_guid=space_guid, host=host, path=path) - return super(RouteManager, self)._create(request) diff --git a/main/cloudfoundry_client/v2/service_bindings.py b/main/cloudfoundry_client/v2/service_bindings.py deleted file mode 100644 index 30c79ba..0000000 --- a/main/cloudfoundry_client/v2/service_bindings.py +++ /dev/null @@ -1,15 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class ServiceBindingManager(EntityManager): - def __init__(self, target_endpoint, client): - super(ServiceBindingManager, self).__init__(target_endpoint, client, '/v2/service_bindings') - - def create(self, app_guid, instance_guid, parameters=None, name=None): - request = self._request(app_guid=app_guid, service_instance_guid=instance_guid) - request['parameters'] = parameters - request['name'] = name - return super(ServiceBindingManager, self)._create(request) - - def remove(self, binding_id): - super(ServiceBindingManager, self)._remove(binding_id) diff --git a/main/cloudfoundry_client/v2/service_brokers.py b/main/cloudfoundry_client/v2/service_brokers.py deleted file mode 100644 index 0f69c85..0000000 --- a/main/cloudfoundry_client/v2/service_brokers.py +++ /dev/null @@ -1,23 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class ServiceBrokerManager(EntityManager): - def __init__(self, target_endpoint, client): - super(ServiceBrokerManager, self).__init__(target_endpoint, client, '/v2/service_brokers') - - def create(self, broker_url, broker_name, auth_username, auth_password, space_guid=None): - request = self._request(broker_url=broker_url, name=broker_name, - auth_username=auth_username, auth_password=auth_password) - request['space_guid'] = space_guid - return super(ServiceBrokerManager, self)._create(request) - - def update(self, broker_guid, broker_url=None, broker_name=None, auth_username=None, auth_password=None): - request = self._request() - request['broker_url'] = broker_url - request['name'] = broker_name - request['auth_username'] = auth_username - request['auth_password'] = auth_password - return super(ServiceBrokerManager, self)._update(broker_guid, request) - - def remove(self, broker_guid): - super(ServiceBrokerManager, self)._remove(broker_guid) diff --git a/main/cloudfoundry_client/v2/service_instances.py b/main/cloudfoundry_client/v2/service_instances.py deleted file mode 100644 index aa91284..0000000 --- a/main/cloudfoundry_client/v2/service_instances.py +++ /dev/null @@ -1,37 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager -from cloudfoundry_client.json_object import JsonObject - - -class ServiceInstanceManager(EntityManager): - list_query_parameters = ['page', 'results-per-page', 'order-direction', 'return_user_provided_service_instances'] - - def __init__(self, target_endpoint, client): - super(ServiceInstanceManager, self).__init__(target_endpoint, client, '/v2/service_instances') - - def create(self, space_guid, instance_name, plan_guid, parameters=None, tags=None, accepts_incomplete=False): - request = self._request(name=instance_name, space_guid=space_guid, service_plan_guid=plan_guid) - request['parameters'] = parameters - request['tags'] = tags - params = None if not accepts_incomplete else dict(accepts_incomplete="true") - return super(ServiceInstanceManager, self)._create(request, params=params) - - def update(self, instance_guid, instance_name=None, plan_guid=None, parameters=None, tags=None, accepts_incomplete=False): - request = self._request() - request['name'] = instance_name - request['service_plan_guid'] = plan_guid - request['parameters'] = parameters - request['tags'] = tags - params = None if not accepts_incomplete else dict(accepts_incomplete="true") - return super(ServiceInstanceManager, self)._update(instance_guid, request, params=params) - - def list_permissions(self, instance_guid): - return super(ServiceInstanceManager, self)._get('%s/%s/permissions' % (self.entity_uri, instance_guid), - JsonObject) - - def remove(self, instance_guid, accepts_incomplete=False, purge=False): - parameters = {} - if accepts_incomplete: - parameters['accepts_incomplete'] = "true" - if purge: - parameters['purge']= "true" - super(ServiceInstanceManager, self)._remove(instance_guid, params=parameters) diff --git a/main/cloudfoundry_client/v2/service_keys.py b/main/cloudfoundry_client/v2/service_keys.py deleted file mode 100644 index 58ff7a8..0000000 --- a/main/cloudfoundry_client/v2/service_keys.py +++ /dev/null @@ -1,14 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class ServiceKeyManager(EntityManager): - def __init__(self, target_endpoint, client): - super(ServiceKeyManager, self).__init__(target_endpoint, client, '/v2/service_keys') - - def create(self, service_instance_guid, name, parameters=None): - request = self._request(service_instance_guid=service_instance_guid, name=name) - request['parameters'] = parameters - return super(ServiceKeyManager, self)._create(request) - - def remove(self, key_guid): - super(ServiceKeyManager, self)._remove(key_guid) diff --git a/main/cloudfoundry_client/v2/service_plan_visibilities.py b/main/cloudfoundry_client/v2/service_plan_visibilities.py deleted file mode 100644 index a1f449d..0000000 --- a/main/cloudfoundry_client/v2/service_plan_visibilities.py +++ /dev/null @@ -1,21 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class ServicePlanVisibilityManager(EntityManager): - def __init__(self, target_endpoint, client): - super(ServicePlanVisibilityManager, self).__init__(target_endpoint, client, '/v2/service_plan_visibilities') - - def create(self, service_plan_guid, organization_guid): - request = self._request() - request['service_plan_guid'] = service_plan_guid - request['organization_guid'] = organization_guid - return super(ServicePlanVisibilityManager, self)._create(request) - - def update(self, spv_guid, service_plan_guid, organization_guid): - request = self._request() - request['service_plan_guid'] = service_plan_guid - request['organization_guid'] = organization_guid - return super(ServicePlanVisibilityManager, self)._update(spv_guid, request) - - def remove(self, spv_guid): - super(ServicePlanVisibilityManager, self)._remove(spv_guid) diff --git a/main/cloudfoundry_client/v2/service_plans.py b/main/cloudfoundry_client/v2/service_plans.py deleted file mode 100644 index 8a6a2a1..0000000 --- a/main/cloudfoundry_client/v2/service_plans.py +++ /dev/null @@ -1,13 +0,0 @@ -from cloudfoundry_client.v2.entities import EntityManager - - -class ServicePlanManager(EntityManager): - def __init__(self, target_endpoint, client): - super(ServicePlanManager, self).__init__(target_endpoint, client, '/v2/service_plans') - - def create_from_resource_file(self, path): - raise NotImplementedError('No creation allowed') - - def list_instances(self, service_plan_guid, **kwargs): - return self.client.v2.service_instances._list('%s/%s/service_instances' % (self.entity_uri, service_plan_guid), - **kwargs) diff --git a/main/cloudfoundry_client/v3/apps.py b/main/cloudfoundry_client/v3/apps.py deleted file mode 100644 index 5cc4146..0000000 --- a/main/cloudfoundry_client/v3/apps.py +++ /dev/null @@ -1,9 +0,0 @@ -from cloudfoundry_client.v3.entities import EntityManager - - -class AppManager(EntityManager): - def __init__(self, target_endpoint, client): - super(AppManager, self).__init__(target_endpoint, client, '/v3/apps') - - def remove(self, application_guid): - super(AppManager, self)._remove(application_guid) diff --git a/main/cloudfoundry_client/v3/buildpacks.py b/main/cloudfoundry_client/v3/buildpacks.py deleted file mode 100644 index 1c78882..0000000 --- a/main/cloudfoundry_client/v3/buildpacks.py +++ /dev/null @@ -1,42 +0,0 @@ -from cloudfoundry_client.v3.entities import EntityManager - - -class BuildpackManager(EntityManager): - def __init__(self, target_endpoint, client): - super(BuildpackManager, self).__init__(target_endpoint, client, '/v3/buildpacks') - - def create(self, name, position=0, enabled=True, locked=False, stack=None, - meta_labels=None, meta_annotations=None): - data = { - 'name': name, - 'position': position, - 'enabled': enabled, - 'locked': locked, - 'stack': stack, - 'metadata': { - 'labels': meta_labels, - 'annotations': meta_annotations - } - } - return super(BuildpackManager, self)._create(data) - - def remove(self, buildpack_guid): - super(BuildpackManager, self)._remove(buildpack_guid) - - def update(self, buildpack_guid, name, position=0, enabled=True, - locked=False, stack=None, meta_labels=None, meta_annotations=None): - data = { - 'name': name, - 'position': position, - 'enabled': enabled, - 'locked': locked, - 'stack': stack, - 'metadata': { - 'labels': meta_labels, - 'annotations': meta_annotations - } - } - return super(BuildpackManager, self)._update(buildpack_guid, data) - - def upload(self, buildpack_guid, buildpack_zip): - return super(BuildpackManager, self)._upload_bits(buildpack_guid, buildpack_zip) diff --git a/main/cloudfoundry_client/v3/entities.py b/main/cloudfoundry_client/v3/entities.py deleted file mode 100644 index 7b38cd1..0000000 --- a/main/cloudfoundry_client/v3/entities.py +++ /dev/null @@ -1,157 +0,0 @@ -import logging -import functools - -from cloudfoundry_client.errors import InvalidEntity -from cloudfoundry_client.imported import quote, reduce -from cloudfoundry_client.json_object import JsonObject -from cloudfoundry_client.request_object import Request - -_logger = logging.getLogger(__name__) - - -class Entity(JsonObject): - def __init__(self, entity_manager, *args, **kwargs): - super(Entity, self).__init__(*args, **kwargs) - try: - def default_method(m, u): - raise NotImplementedError('Unknown method %s for url %s' % (m, u)) - - for link_name, link in self.get('links', {}).items(): - if link_name != 'self': - link_method = link.get('method', 'GET').lower() - ref = link['href'] - if link_method == 'get': - new_method = functools.partial(entity_manager._paginate, ref) if link_name.endswith('s')\ - else functools.partial(entity_manager._get, ref) - elif link_method == 'post': - new_method = functools.partial(entity_manager._post, ref) - elif link_method == 'put': - new_method = functools.partial(entity_manager._put, ref) - elif link_method == 'delete': - new_method = functools.partial(entity_manager._delete, 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) - - -class EntityManager(object): - def __init__(self, target_endpoint, client, entity_uri): - self.target_endpoint = target_endpoint - self.entity_uri = entity_uri - self.client = client - - def _post(self, url, data=None, files=None): - response = self.client.post(url, json=data, files=files) - _logger.debug('POST - %s - %s', url, response.text) - return self._read_response(response) - - def _get(self, url): - response = self.client.get(url) - _logger.debug('GET - %s - %s', url, response.text) - return self._read_response(response) - - def _put(self, data, url): - response = self.client.put(url, json=data) - _logger.debug('PUT - %s - %s', url, response.text) - return self._read_response(response) - - def _patch(self, data, url): - response = self.client.patch(url, json=data) - _logger.debug('PATCH - %s - %s', url, response.text) - return self._read_response(response) - - def _delete(self, url): - response = self.client.delete(url) - _logger.debug('DELETE - %s - %s', url, response.text) - - def _list(self, requested_path, **kwargs): - url_requested = EntityManager._get_url_filtered('%s%s' % (self.target_endpoint, requested_path), **kwargs) - for element in self._paginate(url_requested): - yield element - - def _paginate(self, url_requested): - response = self.client.get(url_requested) - while True: - _logger.debug('GET - %s - %s', url_requested, response.text) - response_json = self._read_response(response) - for resource in response_json['resources']: - yield self._entity(resource) - if 'next' not in response_json['pagination'] \ - or response_json['pagination']['next'] is None \ - or response_json['pagination']['next'].get('href') is None: - break - else: - url_requested = response_json['pagination']['next']['href'] - response = self.client.get(url_requested) - - def _create(self, data): - url = '%s%s' % (self.target_endpoint, self.entity_uri) - return self._post(url, data=data) - - def _upload_bits(self, resource_id, filename): - url = '%s%s/%s/upload' % (self.target_endpoint, self.entity_uri, resource_id) - files = {'bits': (filename, open(filename, 'rb'))} - return self._post(url, files=files) - - def _update(self, resource_id, data): - url = '%s%s/%s' % (self.target_endpoint, self.entity_uri, resource_id) - return self._patch(data, url) - - def _remove(self, resource_id): - url = '%s%s/%s' % (self.target_endpoint, self.entity_uri, resource_id) - self._delete(url) - - def __iter__(self): - return self.list() - - def __getitem__(self, entity_guid): - return self.get(entity_guid) - - def list(self, **kwargs): - return self._list(self.entity_uri, **kwargs) - - def get_first(self, **kwargs): - kwargs.setdefault('per_page', 1) - for entity in self._list(self.entity_uri, **kwargs): - return entity - return None - - def get(self, entity_id, *extra_paths): - 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) - - def _read_response(self, response): - result = response.json(object_pairs_hook=JsonObject) - return self._entity(result) - - @staticmethod - def _request(**mandatory_parameters): - return Request(**mandatory_parameters) - - def _entity(self, result): - if 'guid' in result: - return Entity(self, **result) - else: - return result - - @staticmethod - def _get_url_filtered(url, **kwargs): - def _append_encoded_parameter(parameters, args): - parameter_name, parameter_value = args[0], args[1] - if isinstance(parameter_value, (list, tuple)): - parameters.append('%s=%s' % (parameter_name, quote(','.join(parameter_value)))) - else: - parameters.append('%s=%s' % (parameter_name, quote(str(parameter_value)))) - return parameters - - if len(kwargs) > 0: - return '%s?%s' % (url, - "&".join(reduce(_append_encoded_parameter, sorted(list(kwargs.items())), []))) - else: - return url diff --git a/main/cloudfoundry_client/v3/tasks.py b/main/cloudfoundry_client/v3/tasks.py deleted file mode 100644 index 9e8d07b..0000000 --- a/main/cloudfoundry_client/v3/tasks.py +++ /dev/null @@ -1,17 +0,0 @@ -from cloudfoundry_client.v3.entities import EntityManager - - -class TaskManager(EntityManager): - def __init__(self, target_endpoint, client): - super(TaskManager, self).__init__(target_endpoint, client, '/v3/tasks') - - def create(self, application_guid, command, name=None, disk_in_mb=None, memory_in_mb=None, droplet_guid=None): - request = self._request(command=command) - request['name'] = name - request['disk_in_mb'] = disk_in_mb - request['memory_in_mb'] = memory_in_mb - request['droplet_guid'] = droplet_guid - return self._post('%s/v3/apps/%s/tasks' % (self.target_endpoint, application_guid), data=request) - - def cancel(self, task_guid): - return self._post('%s/v3/tasks/%s/actions/cancel' % (self.target_endpoint, task_guid)) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..de4b40e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2119 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7"}, + {file = "aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9"}, + {file = "aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3"}, + {file = "aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06"}, + {file = "aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14"}, + {file = "aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3"}, + {file = "aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c"}, + {file = "aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc"}, + {file = "aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b"}, + {file = "aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3"}, + {file = "aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b"}, + {file = "aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83"}, + {file = "aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67"}, + {file = "aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + +[[package]] +name = "black" +version = "26.3.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"}, + {file = "black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"}, + {file = "black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac"}, + {file = "black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a"}, + {file = "black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a"}, + {file = "black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff"}, + {file = "black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c"}, + {file = "black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5"}, + {file = "black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e"}, + {file = "black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5"}, + {file = "black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1"}, + {file = "black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f"}, + {file = "black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7"}, + {file = "black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983"}, + {file = "black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb"}, + {file = "black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54"}, + {file = "black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f"}, + {file = "black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56"}, + {file = "black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839"}, + {file = "black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2"}, + {file = "black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78"}, + {file = "black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568"}, + {file = "black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f"}, + {file = "black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c"}, + {file = "black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1"}, + {file = "black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b"}, + {file = "black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=1.0.0" +platformdirs = ">=2" +pytokens = ">=0.4.0,<0.5.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ; sys_platform == \"win32\""] + +[[package]] +name = "certifi" +version = "2026.1.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"}, + {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"}, + {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"}, + {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"}, + {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"}, + {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"}, + {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"}, + {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"}, + {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"}, + {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"}, + {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"}, + {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"}, + {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"}, + {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"}, + {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"}, + {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"}, + {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"}, + {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"}, + {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"}, + {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"}, + {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"}, + {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"}, + {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"}, + {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"}, + {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "docutils" +version = "0.22.4" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + +[[package]] +name = "id" +version = "1.5.0" +description = "A tool for generating OIDC identities" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, + {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, +] + +[package.dependencies] +requests = "*" + +[package.extras] +dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + +[[package]] +name = "idna" +version = "3.15" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, +] + +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.0.2" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_context-6.0.2-py3-none-any.whl", hash = "sha256:55fc21af4b4f9ca94aa643b6ee7fe13b1e4c01abf3aeb98ca4ad9c80b741c786"}, + {file = "jaraco_context-6.0.2.tar.gz", hash = "sha256:953ae8dddb57b1d791bf72ea1009b32088840a7dd19b9ba16443f62be919ee57"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}, + {file = "jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jeepney" +version = "0.9.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[package.extras] +test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] + +[[package]] +name = "keyring" +version = "25.7.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[package.dependencies] +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, + {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, +] + +[[package]] +name = "multidict" +version = "6.7.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349"}, + {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e"}, + {file = "multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36"}, + {file = "multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85"}, + {file = "multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"}, + {file = "multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"}, + {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc"}, + {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721"}, + {file = "multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34"}, + {file = "multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff"}, + {file = "multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81"}, + {file = "multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912"}, + {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184"}, + {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45"}, + {file = "multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8"}, + {file = "multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4"}, + {file = "multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b"}, + {file = "multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec"}, + {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6"}, + {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159"}, + {file = "multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288"}, + {file = "multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17"}, + {file = "multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390"}, + {file = "multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e"}, + {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00"}, + {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb"}, + {file = "multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6"}, + {file = "multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d"}, + {file = "multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6"}, + {file = "multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792"}, + {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842"}, + {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b"}, + {file = "multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f"}, + {file = "multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885"}, + {file = "multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c"}, + {file = "multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000"}, + {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63"}, + {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718"}, + {file = "multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0"}, + {file = "multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13"}, + {file = "multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd"}, + {file = "multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827"}, + {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c"}, + {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40"}, + {file = "multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4"}, + {file = "multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91"}, + {file = "multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f"}, + {file = "multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546"}, + {file = "multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3"}, + {file = "multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nh3" +version = "0.3.2" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d"}, + {file = "nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130"}, + {file = "nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868"}, + {file = "nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93"}, + {file = "nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13"}, + {file = "nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80"}, + {file = "nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6"}, + {file = "nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b"}, + {file = "nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe"}, + {file = "nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104"}, + {file = "nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376"}, +] + +[[package]] +name = "oauth2-client" +version = "1.4.2" +description = "A client library for OAuth2" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "oauth2-client-1.4.2.tar.gz", hash = "sha256:5381900448ff1ae762eb7c65c501002eac46bb5ca2f49477fdfeaf9e9969f284"}, + {file = "oauth2_client-1.4.2-py3-none-any.whl", hash = "sha256:7b938ba8166128a3c4c15ad23ca0c95a2468f8e8b6069d019ebc73360c15c7ca"}, +] + +[package.dependencies] +requests = ">=2.5.0" + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "platformdirs" +version = "4.5.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "polling2" +version = "0.5.0" +description = "Updated polling utility with many configurable options" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "polling2-0.5.0-py2.py3-none-any.whl", hash = "sha256:ad86d56fbd7502f0856cac2d0109d595c18fa6c7fb12c88cee5e5d16c17286c1"}, + {file = "polling2-0.5.0.tar.gz", hash = "sha256:90b7da82cf7adbb48029724d3546af93f21ab6e592ec37c8c4619aedd010e342"}, +] + +[[package]] +name = "propcache" +version = "0.4.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, + {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, + {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, + {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, + {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, + {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, + {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, + {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, + {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, + {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, + {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, + {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, + {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, + {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, + {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, + {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, + {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, + {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, + {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, + {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, + {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, + {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, + {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, + {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, + {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, + {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, + {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, +] + +[[package]] +name = "protobuf" +version = "7.34.1" +description = "" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7"}, + {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b"}, + {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a"}, + {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4"}, + {file = "protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a"}, + {file = "protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c"}, + {file = "protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11"}, + {file = "protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280"}, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, +] + +[package.dependencies] +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + +[[package]] +name = "requests" +version = "2.33.1" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "14.2.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, + {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "secretstorage" +version = "3.5.0" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "tomli" +version = "2.3.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, + {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, + {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, + {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, + {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, + {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, + {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, + {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, + {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, + {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, + {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, + {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, + {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, +] + +[[package]] +name = "twine" +version = "6.2.0" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8"}, + {file = "twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf"}, +] + +[package.dependencies] +id = "*" +keyring = {version = ">=21.2.0", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +packaging = ">=24.0" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[package.extras] +keyring = ["keyring (>=21.2.0)"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] +markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\""} + +[[package]] +name = "urllib3" +version = "2.7.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "websocket-client" +version = "1.9.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, + {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["pytest", "websockets"] + +[[package]] +name = "yarl" +version = "1.22.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, + {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, + {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, + {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, + {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, + {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, + {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, + {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, + {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, + {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, + {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, + {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, + {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, + {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, + {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, + {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, + {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, + {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, + {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, + {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, + {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, + {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, + {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, + {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, + {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, + {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, + {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, + {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, + {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, + {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, + {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, + {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, + {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, + {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, + {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, + {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, + {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, + {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, + {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, + {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, + {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, + {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, + {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, + {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, + {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, + {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, + {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, + {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, + {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, + {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10" +content-hash = "0868efc896949f3bab524ef8479b28d82339ae66af946678a93116a94fe02f87" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..1164fa7 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,5 @@ +[virtualenvs] +create = true +in-project = true +prefer-active-python = true +prompt = "cf-python-client" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1d206ae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[tool.black] +line-length = 130 +exclude = ''' +^/( + (main\/cloudfoundry_client\/dropsonde.*) +)/ +''' + +[tool.poetry] +name = "cloudfoundry_client" +version = "1.40.3" +description = "A client library for CloudFoundry" +authors = ["Benjamin Einaudi "] +readme = "README.rst" +homepage = "https://pypi.org/project/cloudfoundry-client/" +documentation = "https://pypi.org/project/cloudfoundry-client/" +repository = "https://github.com/cloudfoundry-community/cf-python-client" +keywords = ["cloudfoundry", "cf"] + +[tool.poetry.dependencies] +python = ">=3.10" +aiohttp = ">=3.8.0" +protobuf = "7.34.1" +oauth2-client= "1.4.2" +websocket-client= "~1.9.0" +PyYAML = ">=6.0" +requests = ">=2.5.0" +polling2= "0.5.0" + +[tool.poetry.group.dev.dependencies] +black= "26.3.1" +flake8= "7.3.0" +pytest = ">=8.2" +twine = ">=6.0" + +[tool.poetry.scripts] +cloudfoundry-client = "cloudfoundry_client.main.main:main" + +[tool.pytest.ini_options] +console_output_style = "count" +pythonpath = [".", "tests",] +testpaths = ["tests"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 764ad21..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -protobuf==3.6.1 -oauth2-client==1.1.0 -websocket-client==0.53.0 -PyYAML==5.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 5916293..0000000 --- a/setup.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -import shutil -import subprocess -import sys - -from setuptools import setup, find_packages, Command - -src_dir = 'main' -package_directory = 'cloudfoundry_client' -package_name = 'cloudfoundry-client' -loggregator_dir = 'loggregator' -sys.path.insert(0, os.path.realpath(src_dir)) - -version_file = '%s/%s/__init__.py' % (src_dir, package_directory) -with open(version_file, 'r') as f: - for line in f.readlines(): - if line.find('__version__') >= 0: - exec(line) - break - else: - raise AssertionError('Failed to load version from %s' % version_file) - - -def purge_sub_dir(path): - shutil.rmtree(os.path.join(os.path.dirname(__file__), path)) - -if 'test' in sys.argv[1:]: - print('%s added' % os.path.join(os.getcwd(), 'test')) - sys.path.append(os.path.join(os.getcwd(), 'test')) - - - -class GenerateCommand(Command): - description = "generate protobuf class generation" - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - source_path = os.path.join(os.path.dirname(__file__), src_dir, package_directory, loggregator_dir) - for file_protobuf in os.listdir(source_path): - if file_protobuf.endswith('.proto'): - file_path = os.path.join(source_path, file_protobuf) - print('Generating %s from %s' % (file_protobuf, file_path)) - subprocess.call(['protoc', '-I', source_path, '--python_out=%s' % source_path, file_path]) - - -setup(name=package_name, - version=__version__, - zip_safe=True, - packages=find_packages(where=src_dir), - author='Benjamin Einaudi', - author_email='antechrestos@gmail.com', - description='A client library for CloudFoundry', - long_description=open('README.rst').read(), - url='http://github.com/antechrestos/cf-python-client', - classifiers=[ - "Programming Language :: Python", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Topic :: Communications", - ], - entry_points={ - 'console_scripts': [ - 'cloudfoundry-client = %s.main.main:main' % package_directory, - ] - }, - cmdclass=dict(generate=GenerateCommand), - package_dir={package_directory: '%s/%s' % (src_dir, package_directory)}, - install_requires=[requirement.rstrip(' \r\n') for requirement in open('requirements.txt')], - tests_require=[ - 'mock==2.0.0', - ], - test_suite='test', - ) diff --git a/test/abstract_test_case.py b/test/abstract_test_case.py deleted file mode 100644 index b5ac2a4..0000000 --- a/test/abstract_test_case.py +++ /dev/null @@ -1,35 +0,0 @@ -import json - -from oauth2_client.credentials_manager import CredentialManager - -from cloudfoundry_client.client import CloudFoundryClient -from fake_requests import TARGET_ENDPOINT, mock_response -from imported import MagicMock, patch - - -def mock_cloudfoundry_client_class(): - if not getattr(CloudFoundryClient, 'CLASS_MOCKED', False): - mocked_attributes = ['get', 'post', 'patch', 'put', 'delete'] - - class MockClass(CredentialManager): - def __init__(self, *args, **kwargs): - super(MockClass, self).__init__(*args, **kwargs) - for attribute in mocked_attributes: - setattr(self, attribute, MagicMock()) - - CloudFoundryClient.__bases__ = (MockClass,) - setattr(CloudFoundryClient, 'CLASS_MOCKED', True) - - -class AbstractTestCase(object): - @classmethod - def mock_client_class(cls): - mock_cloudfoundry_client_class() - - def build_client(self): - with patch('cloudfoundry_client.client.requests') as fake_requests: - fake_info_response = mock_response('/v2/info', 200, None) - fake_info_response.text = json.dumps(dict(api_version='2.X', - authorization_endpoint=TARGET_ENDPOINT)) - fake_requests.get.return_value = fake_info_response - self.client = CloudFoundryClient(TARGET_ENDPOINT) diff --git a/test/fake_requests.py b/test/fake_requests.py deleted file mode 100644 index 5b477c9..0000000 --- a/test/fake_requests.py +++ /dev/null @@ -1,66 +0,0 @@ -import os -from json import loads - -from imported import SEE_OTHER, iterate_text, MagicMock - - -class MockSession(object): - def __init__(self): - self.headers = dict() - self.proxies = None - self.verify = True - self.trust_env = False - - -class MockResponse(object): - def __init__(self, url, status_code, text, headers=None): - self.status_code = status_code - self.url = url - self.text = text - self.headers = dict() - self.is_redirect = status_code == SEE_OTHER - if headers is not None: - self.headers.update(headers) - - def check_data(self, data, json, **kwargs): - pass - - def json(self, **kwargs): - return loads(self.text, **kwargs) - - def __iter__(self): - return iterate_text(self.text) - - -TARGET_ENDPOINT = "http://somewhere.org" - - -def get_fixtures_path(*paths): - return os.path.join(os.path.dirname(__file__), 'fixtures', *paths) - - -def mock_response(uri, status_code, headers, *path_parts): - global TARGET_ENDPOINT - if len(path_parts) > 0: - file_name = path_parts[len(path_parts) - 1] - extension_idx = file_name.rfind('.') - binary_file = extension_idx >= 0 and file_name[extension_idx:] == '.bin' - with(open(get_fixtures_path(*path_parts), - 'rb' if binary_file else 'r')) as f: - return MockResponse(url='%s%s' % (TARGET_ENDPOINT, uri), - status_code=status_code, - text=f.read(), - headers=headers) - else: - return MockResponse('%s%s' % (TARGET_ENDPOINT, uri), - status_code, - '') - - -class FakeRequests(object): - def __init__(self): - self.Session = MagicMock() - self.post = MagicMock() - self.get = MagicMock() - self.put = MagicMock() - self.patch = MagicMock() diff --git a/test/fixtures/fake/manifest_main.yml b/test/fixtures/fake/manifest_main.yml deleted file mode 100644 index 729ac70..0000000 --- a/test/fixtures/fake/manifest_main.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -applications: -- name: test_app - memory: 1024M - instances: 1 - path: . - health-check-type: http - health-check-http-endpoint: /manage/health - services: - - service1 - - service2 - no-route: true - env: - VAR1: VALUE1 - VAR2: VALUE2 - diff --git a/test/fixtures/v2/apps/GET_{id}_summary_response.json b/test/fixtures/v2/apps/GET_{id}_summary_response.json deleted file mode 100644 index 5bcb834..0000000 --- a/test/fixtures/v2/apps/GET_{id}_summary_response.json +++ /dev/null @@ -1,60 +0,0 @@ - -{ - "version": "0b9e28b8-b69d-46ba-ad7e-6c83899826b2", - "staging_failed_reason": null, - "docker_credentials_json": { - "redacted_message": "[PRIVATE DATA HIDDEN]" - }, - "staging_failed_description": null, - "instances": 1, - "guid": "a5eee659-56a7-4123-91ac-ba190ddc5477", - "docker_image": null, - "diego": true, - "console": false, - "package_state": "STAGED", - "state": "STOPPED", - "production": false, - "stack_guid": "3cb62b55-3230-45d1-a297-ace25b3e3479", - "memory": 512, - "package_updated_at": "2016-08-10T14:01:54Z", - "staging_task_id": "c80457643d7d455c93f92151d6384c2d", - "buildpack": null, - "enable_ssh": true, - "detected_start_command": "sh boot.sh", - "disk_quota": 1024, - "routes": [ - { - "path": "", - "host": "application_name", - "guid": "6c7d07f9-57a1-474e-8e91-82a597550956", - "port": null, - "domain": { - "guid": "f030f4bb-2b60-47e9-be3e-8105655c0286", - "name": "some-domain" - } - } - ], - "services": [], - "detected_buildpack": "staticfile 1.3.8", - "space_guid": "6c99652b-4795-416c-9466-64a3e4555627", - "name": "application_name", - "running_instances": 0, - "health_check_type": "port", - "command": null, - "debug": null, - "available_domains": [ - { - "guid": "255b9370-3836-402c-9e1f-08b2a96dd180", - "name": "some-domain", - "owning_organization_guid": "d7d77408-a250-45e3-8de5-71fcf199bbab" - }, - { - "guid": "f030f4bb-2b60-47e9-be3e-8105655c0286", - "name": "other-domain", - "owning_organization_guid": "d7d77408-a250-45e3-8de5-71fcf199bbab" - } - ], - "environment_json": {}, - "ports": null, - "health_check_timeout": null -} \ No newline at end of file diff --git a/test/fixtures/v3/service_instances/GET_response.json b/test/fixtures/v3/service_instances/GET_response.json deleted file mode 100644 index 708ef6f..0000000 --- a/test/fixtures/v3/service_instances/GET_response.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "pagination": { - "total_results": 1, - "total_pages": 1, - "first": { - "href": "https://api.example.org/v3/service_instances?page=1&per_page=50" - }, - "last": { - "href": "https://api.example.org/v3/service_instances?page=1&per_page=50" - }, - "next": null, - "previous": null - }, - "resources": [ - { - "guid": "85ccdcad-d725-4109-bca4-fd6ba062b5c8", - "created_at": "2017-11-17T13:54:21Z", - "updated_at": "2017-11-17T13:54:21Z", - "name": "my_service_instance", - "relationships": { - "space": { - "data": { - "guid": "ae0031f9-dd49-461c-a945-df40e77c39cb" - } - } - }, - "metadata": { - "labels": { }, - "annotations": { } - }, - "links": { - "space": { - "href": "https://api.example.org/v3/spaces/ae0031f9-dd49-461c-a945-df40e77c39cb" - } - } - } - ] -} \ No newline at end of file diff --git a/test/fixtures/v3/service_instances/GET_{id}_response.json b/test/fixtures/v3/service_instances/GET_{id}_response.json deleted file mode 100644 index 3608a86..0000000 --- a/test/fixtures/v3/service_instances/GET_{id}_response.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "guid": "85ccdcad-d725-4109-bca4-fd6ba062b5c8", - "created_at": "2017-11-17T13:54:21Z", - "updated_at": "2017-11-17T13:54:21Z", - "name": "my_service_instance", - "relationships": { - "space": { - "data": { - "guid": "ae0031f9-dd49-461c-a945-df40e77c39cb" - } - } - }, - "metadata": { - "labels": { }, - "annotations": { } - }, - "links": { - "space": { - "href": "https://api.example.org/v3/spaces/ae0031f9-dd49-461c-a945-df40e77c39cb" - } - } -} \ No newline at end of file diff --git a/test/imported.py b/test/imported.py deleted file mode 100644 index 643e014..0000000 --- a/test/imported.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys - -if sys.version_info.major == 2: - from mock import patch, call, MagicMock, mock_open - from httplib import SEE_OTHER, CREATED, NO_CONTENT, ACCEPTED - def iterate_text(text): - for character in text: - yield character - built_in_entry = '__builtin__' - -elif sys.version_info.major == 3: - from unittest.mock import patch, call, MagicMock, mock_open - from http import HTTPStatus - SEE_OTHER = HTTPStatus.SEE_OTHER.value - CREATED = HTTPStatus.CREATED.value - NO_CONTENT = HTTPStatus.NO_CONTENT.value - ACCEPTED = HTTPStatus.ACCEPTED.value - def iterate_text(text): - for character in text: - yield bytes([character]) - built_in_entry = 'builtins' -else: - raise ImportError('Invalid major version: %d' % sys.version_info.major) \ No newline at end of file diff --git a/test/operations/push/test_push.py b/test/operations/push/test_push.py deleted file mode 100644 index c4ec3ab..0000000 --- a/test/operations/push/test_push.py +++ /dev/null @@ -1,59 +0,0 @@ -import sys -from unittest import TestCase - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.operations.push.push import PushOperation -from fake_requests import get_fixtures_path -from imported import patch, MagicMock - - -class TestPushOperation(TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_split_route_with_port_and_path(self): - domain, port, path = PushOperation._split_route(dict(route='foo-((suffix)).apps.internal:666/some/path')) - self.assertEqual('foo-((suffix)).apps.internal', domain) - self.assertEqual(666, port) - self.assertEqual('/some/path', path) - - def test_split_route_without_port_and_path(self): - domain, port, path = PushOperation._split_route(dict(route='foo-((suffix)).apps.internal')) - self.assertEqual('foo-((suffix)).apps.internal', domain) - self.assertIsNone(port) - self.assertEqual('', path) - - def test_split_route_without_port_path(self): - domain, port, path = PushOperation._split_route(dict(route='foo-((suffix)).apps.internal/path')) - self.assertEqual('foo-((suffix)).apps.internal', domain) - self.assertIsNone(port) - self.assertEqual('/path', path) - - def test_split_route_without_path(self): - domain, port, path = PushOperation._split_route(dict(route='foo-((suffix)).apps.internal:666')) - self.assertEqual('foo-((suffix)).apps.internal', domain) - self.assertEqual(666, port) - self.assertEqual('', path) - - def test_to_host_should_remove_unwanted_characters(self): - host = PushOperation._to_host('idzone-3.0.7-rec-tb1_bobby') - self.assertEquals('idzone-307-rec-tb1-bobby', host) - - @patch.object(sys, 'argv', ['main', 'push_app', get_fixtures_path('fake', 'manifest_main.yml'), '-space_guid', - 'space_id']) - def test_main_push(self): - class FakeOperation(object): - def __init__(self): - self.push = MagicMock() - client = object() - push_operation = FakeOperation() - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: client), \ - patch('cloudfoundry_client.main.operation_commands.PushOperation', new=lambda c: push_operation): - main.main() - push_operation.push.assert_called_with('space_id', get_fixtures_path('fake', 'manifest_main.yml')) diff --git a/test/operations/push/validation/test_manifest_reader.py b/test/operations/push/validation/test_manifest_reader.py deleted file mode 100644 index 0ab60c0..0000000 --- a/test/operations/push/validation/test_manifest_reader.py +++ /dev/null @@ -1,101 +0,0 @@ -import os -import unittest - -from cloudfoundry_client.operations.push.validation.manifest import ManifestReader - - -class TestManifestReader(unittest.TestCase): - def test_empty_manifest_should_raise_exception(self): - manifest_file = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'fixtures', 'operations', - 'manifest_empty.yml') - self.assertRaises(AssertionError, lambda: ManifestReader.load_application_manifests(manifest_file)) - - def test_manifest_should_be_read(self): - manifest_file = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'fixtures', 'operations', - 'manifest.yml') - applications = ManifestReader.load_application_manifests(manifest_file) - self.assertEqual(1, len(applications)) - self.assertEqual(dict(docker=dict(username='the-user', password='P@SsW0r$', image='some-image'), - name='the-name', routes=[dict(route='first-route'), dict(route='second-route')]), - applications[0]) - - def test_complex_manifest_should_be_read(self): - manifest_file = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'fixtures', 'operations', - 'manifest_complex.yml') - applications = ManifestReader.load_application_manifests(manifest_file) - self.assertEqual(2, len(applications)) - self.assertEqual(dict(name='bigapp', buildpacks=['staticfile_buildpack'], memory=1024, - path=os.path.abspath(os.path.join(os.path.dirname(manifest_file), 'big'))), - applications[0]) - self.assertEqual(dict(name='smallapp', buildpacks=['staticfile_buildpack'], memory=256, - path=os.path.abspath(os.path.join(os.path.dirname(manifest_file), 'small'))), - applications[1]) - - def test_name_should_be_set(self): - manifest = dict(path='test/') - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_application_should_declare_either_path_or_docker(self): - manifest = dict(name='the-name', docker=dict(), path='test/') - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_application_should_declare_at_least_path_or_docker(self): - manifest = dict(name='the-name', routes=[], environment=dict()) - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_deprecated_entries_should_not_be_set(self): - for deprecated in ['host', 'hosts', 'domain', 'domains', 'no-hostname']: - manifest = dict(name='the-name', path='test/') - manifest[deprecated] = 'some-value' - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_docker_manifest_should_declare_buildpack_or_image(self): - manifest = dict(name='the-name', docker=dict(image='some-image', buildpack='some-buildpack')) - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_username_should_be_set_if_password_is(self): - manifest = dict(name='the-name', docker=dict(image='some-image', password='P@SsW0r$')) - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_password_should_be_set_if_username_is(self): - manifest = dict(name='the-name', docker=dict(image='some-image', username='the-user')) - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_username_and_password_are_set_when_image_is(self): - manifest = dict(name='the-name', docker=dict(username='the-user', password='P@SsW0r$')) - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_routes_should_be_an_object_with_attribute(self): - manifest = dict(name='the-name', path='test/', routes=['a route']) - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - manifest = dict(name='the-name', path='test/', routes=[dict(invalid_attribute='any-value')]) - self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest('.', manifest)) - - def test_valid_application_with_path_and_routes(self): - manifest = dict(name='the-name', path='test/', routes=[dict(route='first-route'), dict(route='second-route')]) - ManifestReader._validate_application_manifest('.', manifest) - - def test_valid_application_with_docker_and_routes(self): - manifest = dict(docker=dict(username='the-user', password='P@SsW0r$', image='some-image'), - name='the-name', routes=[dict(route='first-route'), dict(route='second-route')]) - ManifestReader._validate_application_manifest('.', manifest) - - def path_should_be_set_as_absolute(self): - manifest = dict(name='the-name', path='test/') - ManifestReader._validate_application_manifest('.', manifest) - self.assertEqual(os.path.abspath('test'), manifest['path']) - - def test_memory_in_kb(self): - manifest = dict(memory='2048KB') - ManifestReader._convert_memory(manifest) - self.assertEqual(2, manifest['memory']) - - def test_memory_in_mb(self): - manifest = dict(memory='2048MB') - ManifestReader._convert_memory(manifest) - self.assertEqual(2048, manifest['memory']) - - def test_memory_in_gb(self): - manifest = dict(memory='1G') - ManifestReader._convert_memory(manifest) - self.assertEqual(1024, manifest['memory']) \ No newline at end of file diff --git a/test/requirements.txt b/test/requirements.txt deleted file mode 100644 index 59b3290..0000000 --- a/test/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# only on python 2 -mock==2.0.0 diff --git a/test/test_client.py b/test/test_client.py deleted file mode 100644 index 528f487..0000000 --- a/test/test_client.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.client import CloudFoundryClient -from cloudfoundry_client.imported import OK -from cloudfoundry_client.imported import quote -from fake_requests import MockResponse, MockSession, FakeRequests -from fake_requests import TARGET_ENDPOINT -from imported import patch - - -class TestCloudfoundryClient(unittest.TestCase, AbstractTestCase,): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - - def test_grant_password_request_with_token_format_opaque(self): - requests = FakeRequests() - session = MockSession() - with patch('oauth2_client.credentials_manager.requests', new=requests), \ - patch('cloudfoundry_client.client.requests', new=requests): - requests.Session.return_value = session - requests.get.return_value = MockResponse('%s/v2/info' % TARGET_ENDPOINT, - status_code=OK, - text=json.dumps(dict(api_version='2.1', - authorization_endpoint=TARGET_ENDPOINT))) - requests.post.return_value = MockResponse('%s/oauth/token' % TARGET_ENDPOINT, - status_code=OK, - text=json.dumps(dict(access_token='access-token', - refresh_token='refresh-token'))) - client = CloudFoundryClient(TARGET_ENDPOINT, token_format='opaque') - client.init_with_user_credentials('somebody', 'p@s$w0rd') - self.assertEqual('Bearer access-token', session.headers.get('Authorization')) - requests.post.assert_called_with(requests.post.return_value.url, - data=dict(grant_type='password', - username='somebody', - scope='', - password='p@s$w0rd', - token_format='opaque'), - headers=dict(Accept='application/json', Authorization='Basic Y2Y6'), - proxies=dict(http='', https=''), - verify=True) - - def test_refresh_request_with_token_format_opaque(self): - requests = FakeRequests() - session = MockSession() - with patch('oauth2_client.credentials_manager.requests', new=requests), \ - patch('cloudfoundry_client.client.requests', new=requests): - requests.Session.return_value = session - requests.get.return_value = MockResponse('%s/v2/info' % TARGET_ENDPOINT, - status_code=OK, - text=json.dumps(dict(api_version='2.1', - authorization_endpoint=TARGET_ENDPOINT))) - requests.post.return_value = MockResponse('%s/oauth/token' % TARGET_ENDPOINT, - status_code=OK, - text=json.dumps(dict(access_token='access-token', - refresh_token='refresh-token'))) - client = CloudFoundryClient(TARGET_ENDPOINT, token_format='opaque') - client.init_with_token('refresh-token') - self.assertEqual('Bearer access-token', session.headers.get('Authorization')) - requests.post.assert_called_with(requests.post.return_value.url, - data=dict(grant_type='refresh_token', - scope='', - refresh_token='refresh-token', - token_format='opaque'), - headers=dict(Accept='application/json', Authorization='Basic Y2Y6'), - proxies=dict(http='', https=''), - verify=True) - - def test_grant_password_request_with_login_hint(self): - requests = FakeRequests() - session = MockSession() - with patch('oauth2_client.credentials_manager.requests', new=requests), \ - patch('cloudfoundry_client.client.requests', new=requests): - requests.Session.return_value = session - requests.get.return_value = MockResponse('%s/v2/info' % TARGET_ENDPOINT, - status_code=OK, - text=json.dumps(dict(api_version='2.1', - authorization_endpoint=TARGET_ENDPOINT))) - requests.post.return_value = MockResponse('%s/oauth/token' % TARGET_ENDPOINT, - status_code=OK, - text=json.dumps(dict(access_token='access-token', - refresh_token='refresh-token'))) - client = CloudFoundryClient(TARGET_ENDPOINT, login_hint=quote(json.dumps(dict(origin='uaa'), - separators=(',', ':')))) - client.init_with_user_credentials('somebody', 'p@s$w0rd') - self.assertEqual('Bearer access-token', session.headers.get('Authorization')) - requests.post.assert_called_with(requests.post.return_value.url, - data=dict(grant_type='password', - username='somebody', - scope='', - password='p@s$w0rd', - login_hint='%7B%22origin%22%3A%22uaa%22%7D'), - headers=dict(Accept='application/json', Authorization='Basic Y2Y6'), - proxies=dict(http='', https=''), - verify=True) diff --git a/test/test_request_object.py b/test/test_request_object.py deleted file mode 100644 index e443048..0000000 --- a/test/test_request_object.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - -from cloudfoundry_client.request_object import Request - - -class TestRequest(unittest.TestCase): - - def test_mandatory_should_be_present_even_when_none(self): - request = Request(mandatory=None) - self.assertTrue('mandatory' in request) - self.assertIsNone(request['mandatory']) - - def test_optional_should_not_be_present_when_none(self): - request = Request(mandatory='value') - request['optional'] = None - self.assertEqual('value', request['mandatory']) - self.assertTrue('optional' not in request) diff --git a/test/v2/test_apps.py b/test/v2/test_apps.py deleted file mode 100644 index 4391c20..0000000 --- a/test/v2/test_apps.py +++ /dev/null @@ -1,288 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.errors import InvalidStatusCode -from cloudfoundry_client.imported import BAD_REQUEST, OK, reduce -from fake_requests import mock_response, TARGET_ENDPOINT -from imported import CREATED, patch, call, NO_CONTENT - - -class TestApps(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v2/apps', - OK, - None, - 'v2', 'apps', 'GET_response.json') - all_applications = [application for application in self.client.v2.apps.list()] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(len(all_applications), 3) - print('test_list - Application - %s' % str(all_applications[0])) - self.assertEqual(all_applications[0]['entity']['name'], "name-423") - - def test_list_filtered(self): - self.client.get.return_value = mock_response( - '/v2/apps?q=name%3Aapplication_name&results-per-page=1&q=space_guid%3Aspace_guid', - OK, - None, - 'v2', 'apps', 'GET_space_guid_name_response.json') - application = self.client.v2.apps.get_first(space_guid='space_guid', name='application_name') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(application) - - def test_get_env(self): - self.client.get.return_value = mock_response( - '/v2/apps/app_id/env', - OK, - None, - 'v2', 'apps', 'GET_{id}_env_response.json') - application = self.client.v2.apps.get_env('app_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(application) - - def test_get_instances(self): - self.client.get.return_value = mock_response( - '/v2/apps/app_id/instances', - OK, - None, - 'v2', 'apps', 'GET_{id}_instances_response.json') - application = self.client.v2.apps.get_instances('app_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(application) - - def test_get_stats(self): - self.client.get.return_value = mock_response( - '/v2/apps/app_id/stats', - OK, - None, - 'v2', 'apps', 'GET_{id}_stats_response.json') - application = self.client.v2.apps.get_stats('app_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(application) - - def test_associate_route(self): - self.client.put.return_value = mock_response('/v2/apps/app_id/routes/route_id', CREATED, None, - 'v2', 'apps', 'PUT_{id}_routes_{route_id}_response.json') - self.client.v2.apps.associate_route('app_id', 'route_id') - self.client.put.assert_called_with(self.client.put.return_value.url, json=None) - - def test_list_routes(self): - self.client.get.return_value = mock_response( - '/v2/apps/app_id/routes?q=route_guid%3Aroute_id', - OK, - None, - 'v2', 'apps', 'GET_{id}_routes_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.apps.list_routes('app_id', route_guid='route_id'), 0) - for route in self.client.v2.apps.list_routes('app_id', route_guid='route_id'): - print(route) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_remove_route(self): - self.client.delete.return_value = mock_response('/v2/apps/app_id/routes/route_id', NO_CONTENT, None) - self.client.v2.apps.remove_route('app_id', 'route_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - def test_list_service_bindings(self): - self.client.get.return_value = mock_response( - '/v2/apps/app_id/service_bindings', - OK, - None, - 'v2', 'apps', 'GET_{id}_service_bindings_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.apps.list_service_bindings('app_id'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get_sumary(self): - self.client.get.return_value = mock_response( - '/v2/apps/app_id/summary', - OK, - None, - 'v2', 'apps', 'GET_{id}_summary_response.json') - application = self.client.v2.apps.get_summary('app_id') - - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(application) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/apps/app_id', - OK, - None, - 'v2', 'apps', 'GET_{id}_response.json') - application = self.client.v2.apps.get('app_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(application) - - def test_start(self): - self.client.put.return_value = mock_response( - '/v2/apps/app_id', - CREATED, - None, - 'v2', 'apps', 'PUT_{id}_response.json') - mock_summary = mock_response( - '/v2/apps/app_id/summary', - OK, - None, - 'v2', 'apps', 'GET_{id}_summary_response.json') - mock_instances_stopped = mock_response( - '/v2/apps/app_id/instances', - BAD_REQUEST, - None, - 'v2', 'apps', 'GET_{id}_instances_stopped_response.json') - mock_instances_started = mock_response( - '/v2/apps/app_id/instances', - OK, - None, - 'v2', 'apps', 'GET_{id}_instances_response.json') - self.client.get.side_effect = [mock_summary, - InvalidStatusCode(BAD_REQUEST, dict(code=220001)), - mock_instances_started] - - application = self.client.v2.apps.start('app_id') - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(state='STARTED')) - self.client.get.assert_has_calls([call(mock_summary.url), - call(mock_instances_stopped.url), - call(mock_instances_started.url)], - any_order=False) - self.assertIsNotNone(application) - - def test_stop(self): - self.client.put.return_value = mock_response( - '/v2/apps/app_id', - CREATED, - None, - 'v2', 'apps', 'PUT_{id}_response.json') - self.client.get.side_effect = [InvalidStatusCode(BAD_REQUEST, dict(code=220001))] - application = self.client.v2.apps.stop('app_id') - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(state='STOPPED')) - self.client.get.assert_called_with('%s/v2/apps/app_id/instances' % TARGET_ENDPOINT) - self.assertIsNotNone(application) - - def test_create(self): - self.client.post.return_value = mock_response( - '/v2/apps', - CREATED, - None, - 'v2', 'apps', 'POST_response.json') - application = self.client.v2.apps.create(name='test', space_guid='1fbb3e81-4f55-4fd3-9820-45febbd5e53e', - stack_guid='82f9c01c-72f2-4d3e-b5ed-eab97a6203cf', memory=1024, - instances=1, - disk_quota=1024, health_check_type="port") - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(name='test', space_guid='1fbb3e81-4f55-4fd3-9820-45febbd5e53e', - stack_guid='82f9c01c-72f2-4d3e-b5ed-eab97a6203cf', memory=1024, - instances=1, disk_quota=1024, health_check_type="port")) - self.assertIsNotNone(application) - - def test_update(self): - self.client.put.return_value = mock_response( - '/v2/apps/app_id', - CREATED, - None, - 'v2', 'apps', 'PUT_{id}_response.json') - application = self.client.v2.apps.update('app_id', stack_guid='82f9c01c-72f2-4d3e-b5ed-eab97a6203cf', - memory=1024, - instances=1, disk_quota=1024, health_check_type="port") - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(stack_guid='82f9c01c-72f2-4d3e-b5ed-eab97a6203cf', memory=1024, - instances=1, disk_quota=1024, health_check_type="port")) - self.assertIsNotNone(application) - - def test_remove(self): - self.client.delete.return_value = mock_response( - '/v2/apps/app_id', - NO_CONTENT, None) - self.client.v2.apps.remove('app_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - def test_restage(self): - self.client.post.return_value = mock_response( - '/v2/apps/app_id/restage', - CREATED, - None, - 'v2', 'apps', 'POST_{id}_restage_response.json') - self.client.v2.apps.restage('app_id') - self.client.post.assert_called_with(self.client.post.return_value.url, json=None) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/apps/app_id', - OK, - None, - 'v2', 'apps', 'GET_{id}_response.json'), - mock_response( - '/v2/spaces/space_id', - OK, - None, - 'v2', 'spaces', 'GET_{id}_response.json') - , - mock_response( - '/v2/routes', - OK, - None, - 'v2', 'routes', 'GET_response.json') - ] - application = self.client.v2.apps.get('app_id') - - self.assertIsNotNone(application.space()) - cpt = reduce(lambda increment, _: increment + 1, application.routes(), 0) - self.assertEqual(cpt, 1) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_apps']) - def test_main_list_apps(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/apps', - OK, - None, - 'v2', 'apps', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'delete_app', '906775ea-622e-4bc7-af5d-9aab3b652f81']) - def test_main_delete_apps(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.delete.return_value = mock_response('/v2/apps/906775ea-622e-4bc7-af5d-9aab3b652f81', - NO_CONTENT, - None) - main.main() - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_app', '906775ea-622e-4bc7-af5d-9aab3b652f81']) - def test_main_get_app(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/apps/906775ea-622e-4bc7-af5d-9aab3b652f81', - OK, - None, - 'v2', 'apps', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'restage', '906775ea-622e-4bc7-af5d-9aab3b652f81']) - def test_main_restage_app(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.post.return_value = mock_response('/v2/apps/906775ea-622e-4bc7-af5d-9aab3b652f81/restage', - CREATED, - None, - 'v2', 'apps', 'POST_{id}_restage_response.json') - main.main() - self.client.post.assert_called_with(self.client.post.return_value.url, json=None) diff --git a/test/v2/test_buildpacks.py b/test/v2/test_buildpacks.py deleted file mode 100644 index c63d6fb..0000000 --- a/test/v2/test_buildpacks.py +++ /dev/null @@ -1,45 +0,0 @@ -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import CREATED, patch - - -class TestBuildpacks(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v2/buildpacks', - OK, - None, - 'v2', 'buildpacks', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, self.client.v2.buildpacks.list(), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 3) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/buildpacks/buildpack_id', - OK, - None, - 'v2', 'buildpacks', 'GET_{id}_response.json') - result = self.client.v2.buildpacks.get('buildpack_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_update(self): - self.client.put.return_value = mock_response( - '/v2/buildpacks/build_pack_id', - CREATED, - None, - 'v2', 'apps', 'PUT_{id}_response.json') - result = self.client.v2.buildpacks.update('build_pack_id', dict(enabled=False)) - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(enabled=False)) - self.assertIsNotNone(result) diff --git a/test/v2/test_doppler.py b/test/v2/test_doppler.py deleted file mode 100644 index 82fe3ae..0000000 --- a/test/v2/test_doppler.py +++ /dev/null @@ -1,32 +0,0 @@ -import re -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.doppler.client import DopplerClient -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response, TARGET_ENDPOINT - - -class TestLoggregator(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - self.doppler = DopplerClient(re.sub('^http', 'ws', TARGET_ENDPOINT), - proxy='', - verify_ssl=False, - credentials_manager=self.client) - - def test_recents(self): - boundary = 'd661b2c1426a3abcf1c0524d7fdbc774c42a767bdd6702141702d16047bc' - app_guid = 'app_id' - self.client.get.return_value = mock_response('/apps/%s/recentlogs' % app_guid, - OK, - {'content-type': - 'multipart/x-protobuf; boundary=%s' % boundary}, - 'recents', 'GET_response.bin') - cpt = reduce(lambda increment, _: increment + 1, self.doppler.recent_logs(app_guid), 0) - self.client.get.assert_called_with(self.client.get.return_value.url, stream=True) - self.assertEqual(cpt, 200) diff --git a/test/v2/test_entities.py b/test/v2/test_entities.py deleted file mode 100644 index f303fca..0000000 --- a/test/v2/test_entities.py +++ /dev/null @@ -1,88 +0,0 @@ -import unittest - -from cloudfoundry_client.v2.entities import EntityManager -from cloudfoundry_client.imported import OK, reduce -from fake_requests import TARGET_ENDPOINT, mock_response -from imported import MagicMock, call - - -class TestEntities(unittest.TestCase): - def test_query(self): - url = EntityManager('http://cf.api', None, '/v2/apps')._get_url_filtered('/v2/apps', **{'results-per-page': 20, - 'order-direction': 'asc', - 'page': 1, - 'space_guid': 'id', - 'order-by': 'id'}) - self.assertEqual('/v2/apps?order-by=id&order-direction=asc&page=1&results-per-page=20&q=space_guid%3Aid', url) - - def test_query_multi_order_by(self): - url = EntityManager('http://cf.api', None, '/v2/apps')._get_url_filtered('/v2/apps', **{'order-by': ['timestamp', 'id']}) - self.assertEqual('/v2/apps?order-by=timestamp&order-by=id', url) - - def test_query_single_order_by(self): - url = EntityManager('http://cf.api', None, '/v2/apps')._get_url_filtered('/v2/apps', **{'order-by': 'timestamp'}) - self.assertEqual('/v2/apps?order-by=timestamp', url) - - def test_query_in(self): - url = EntityManager('http://cf.api', None, '/v2/apps')._get_url_filtered('/v2/apps', **{'results-per-page': 20, - 'order-direction': 'asc', - 'page': 1, - 'space_guid': ['id1', 'id2']}) - self.assertEqual('/v2/apps?order-direction=asc&page=1&results-per-page=20&q=space_guid%20IN%20id1%2Cid2', url) - - def test_multi_query(self): - url = EntityManager('http://cf.api', None, '/v2/events')._get_url_filtered('/v2/events', **{'type': ['create', 'update'], - 'organization_guid': 'org-id'}) - self.assertEqual('/v2/events?q=organization_guid%3Aorg-id&q=type%20IN%20create%2Cupdate', url) - - def test_list(self): - client = MagicMock() - entity_manager = EntityManager(TARGET_ENDPOINT, client, '/fake/first') - - first_response = mock_response( - '/fake/first?order-direction=asc&page=1&results-per-page=20&q=space_guid%3Asome-id', - OK, - None, - 'fake', 'GET_multi_page_0_response.json') - second_response = mock_response('/fake/next?order-direction=asc&page=2&results-per-page=50', - OK, - None, - 'fake', 'GET_multi_page_1_response.json') - - client.get.side_effect = [first_response, second_response] - cpt = reduce(lambda increment, _: increment + 1, entity_manager.list(**{"results-per-page": 20, - 'order-direction': 'asc', - 'page': 1, - "space_guid": 'some-id'}), 0) - client.get.assert_has_calls([call(first_response.url), - call(second_response.url)], - any_order=False) - self.assertEqual(cpt, 3) - - def test_iter(self): - client = MagicMock() - entity_manager = EntityManager(TARGET_ENDPOINT, client, '/fake/something') - - client.get.return_value = mock_response( - '/fake/something', - OK, - None, - 'fake', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, entity_manager, 0) - client.get.assert_called_with(client.get.return_value.url) - - self.assertEqual(cpt, 2) - - def test_get_elem(self): - client = MagicMock() - entity_manager = EntityManager(TARGET_ENDPOINT, client, '/fake/something') - - client.get.return_value = mock_response( - '/fake/something/with-id', - OK, - None, - 'fake', 'GET_{id}_response.json') - entity = entity_manager['with-id'] - client.get.assert_called_with(client.get.return_value.url) - - self.assertEqual(entity['entity']['name'], 'name-423') diff --git a/test/v2/test_events.py b/test/v2/test_events.py deleted file mode 100644 index 220eb16..0000000 --- a/test/v2/test_events.py +++ /dev/null @@ -1,25 +0,0 @@ -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK -from fake_requests import mock_response - - -class TestEvents(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v2/events?q=type%3Aaudit.route.delete-request', - OK, - None, - 'v2', 'events', 'GET_response_audit.route.delete-request.json') - delete_route_events = [event for event in self.client.v2.event.list_by_type('audit.route.delete-request')] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(len(delete_route_events), 1) - print('test_list - Event - %s' % str(delete_route_events[0])) - self.assertEqual(delete_route_events[0]['entity']['type'], "audit.route.delete-request") diff --git a/test/v2/test_organizations.py b/test/v2/test_organizations.py deleted file mode 100644 index bdebf9c..0000000 --- a/test/v2/test_organizations.py +++ /dev/null @@ -1,77 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, call - - -class TestOrganizations(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v2/organizations?q=name%3Aorganization_name', - OK, - None, - 'v2', 'organizations', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, self.client.v2.organizations.list(name='organization_name'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/organizations/org_id', - OK, - None, - 'v2', 'organizations', 'GET_{id}_response.json') - result = self.client.v2.organizations.get('org_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/organizations/org_id', - OK, - None, - 'v2', 'organizations', 'GET_{id}_response.json'), - mock_response( - '/v2/organizations/fe79371b-39b8-4f0d-8331-cff423a06aca/spaces', - OK, - None, - 'v2', 'spaces', 'GET_response.json') - ] - organization = self.client.v2.organizations.get('org_id') - cpt = reduce(lambda increment, _: increment + 1, organization.spaces(), 0) - self.assertEqual(cpt, 1) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_organizations']) - def test_main_list_organizations(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/organizations', - OK, - None, - 'v2', 'organizations', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_organization', 'fe79371b-39b8-4f0d-8331-cff423a06aca']) - def test_main_get_organization(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/organizations/fe79371b-39b8-4f0d-8331-cff423a06aca', - OK, - None, - 'v2', 'organizations', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v2/test_routes.py b/test/v2/test_routes.py deleted file mode 100644 index 1f96555..0000000 --- a/test/v2/test_routes.py +++ /dev/null @@ -1,90 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, call - - -class TestRoutes(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response( - '/v2/routes?q=organization_guid%3Aorganization_guid', - OK, - None, - 'v2', 'routes', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, self.client.v2.routes.list(organization_guid='organization_guid'), - 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/routes/route_id', - OK, - None, - 'v2', 'routes', 'GET_{id}_response.json') - result = self.client.v2.routes.get('route_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/routes/route_id', - OK, - None, - 'v2', 'routes', 'GET_{id}_response.json'), - mock_response( - '/v2/service_instances/e3db4ea8-ab0c-4c47-adf8-a70a8e990ee4', - OK, - None, - 'v2', 'service_instances', 'GET_{id}_response.json'), - mock_response( - '/v2/spaces/b3f94ab9-1520-478b-a6d6-eb467c179ada', - OK, - None, - 'v2', 'spaces', 'GET_{id}_response.json'), - mock_response('/v2/routes/75c16cfe-9b8a-4faf-bb65-02c713c7956f/apps', - OK, - None, - 'v2', 'apps', 'GET_response.json') - ] - route = self.client.v2.routes.get('route_id') - self.assertIsNotNone(route.service_instance()) - self.assertIsNotNone(route.space()) - cpt = reduce(lambda increment, _: increment + 1, route.apps(), 0) - self.assertEqual(cpt, 3) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_routes']) - def test_main_list_routes(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/routes', - OK, - None, - 'v2', 'routes', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_route', '75c16cfe-9b8a-4faf-bb65-02c713c7956f']) - def test_main_get_route(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/routes/75c16cfe-9b8a-4faf-bb65-02c713c7956f', - OK, - None, - 'v2', 'routes', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v2/test_service_bindings.py b/test/v2/test_service_bindings.py deleted file mode 100644 index 8b5cb02..0000000 --- a/test/v2/test_service_bindings.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, call, CREATED, NO_CONTENT - - -class TestServiceBindings(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response( - '/v2/service_bindings?q=service_instance_guid%3Ainstance_guid', - OK, - None, - 'v2', 'service_bindings', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.service_bindings.list(service_instance_guid='instance_guid'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/service_bindings/service_binding_id', - OK, - None, - 'v2', 'service_bindings', 'GET_{id}_response.json') - result = self.client.v2.service_bindings.get('service_binding_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_create(self): - self.client.post.return_value = mock_response( - '/v2/service_bindings', - CREATED, - None, - 'v2', 'service_bindings', 'POST_response.json') - service_binding = self.client.v2.service_bindings.create('app_guid', 'instance_guid', - dict(the_service_broker='wants this object'), - 'binding_name') - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(app_guid='app_guid', - service_instance_guid='instance_guid', - name='binding_name', - parameters=dict( - the_service_broker='wants this object'))) - self.assertIsNotNone(service_binding) - - def test_delete(self): - self.client.delete.return_value = mock_response( - '/v2/service_bindings/binding_id', - NO_CONTENT, - None) - self.client.v2.service_bindings.remove('binding_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/service_bindings/service_binding_id', - OK, - None, - 'v2', 'service_bindings', 'GET_{id}_response.json'), - mock_response( - '/v2/service_instances/ef0bf611-82c6-4603-99fc-3a1a893109d0', - OK, - None, - 'v2', 'service_instances', 'GET_{id}_response.json'), - mock_response( - '/v2/apps/c77953c8-6c35-46c7-816e-cf0c42ac2f52', - OK, - None, - 'v2', 'apps', 'GET_{id}_response.json') - ] - service_binding = self.client.v2.service_bindings.get('service_binding_id') - self.assertIsNotNone(service_binding.service_instance()) - self.assertIsNotNone(service_binding.app()) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_service_bindings']) - def test_main_list_service_bindings(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_bindings', - OK, - None, - 'v2', 'service_bindings', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_service_binding', 'eaabd042-8f5c-44a2-9580-1e114c36bdcb']) - def test_main_get_service_binding(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_bindings/eaabd042-8f5c-44a2-9580-1e114c36bdcb', - OK, - None, - 'v2', 'service_bindings', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v2/test_service_brokers.py b/test/v2/test_service_brokers.py deleted file mode 100644 index 97c1ead..0000000 --- a/test/v2/test_service_brokers.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, CREATED, NO_CONTENT - - -class TestServiceBrokers(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response( - '/v2/service_brokers?q=space_guid%3Aspace_guid', - OK, - None, - 'v2', 'service_bindings', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.service_brokers.list(space_guid='space_guid'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/service_brokers/broker_id', - OK, - None, - 'v2', 'service_brokers', 'GET_{id}_response.json') - result = self.client.v2.service_brokers.get('broker_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_create(self): - self.client.post.return_value = mock_response( - '/v2/service_brokers', - CREATED, - None, - 'v2', 'service_brokers', 'POST_response.json') - service_broker = self.client.v2.service_brokers.create('url', 'name', 'username', 'P@sswd1') - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(broker_url='url', - name='name', - auth_username='username', - auth_password='P@sswd1')) - self.assertIsNotNone(service_broker) - - def test_update(self): - self.client.put.return_value = mock_response( - '/v2/service_brokers/broker_id', - OK, - None, - 'v2', 'service_brokers', 'PUT_{id}_response.json') - service_broker = self.client.v2.service_brokers.update('broker_id', - broker_url='new-url', - auth_username='new-username', - auth_password='P@sswd2') - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(broker_url='new-url', - auth_username='new-username', - auth_password='P@sswd2')) - self.assertIsNotNone(service_broker) - - def test_delete(self): - self.client.delete.return_value = mock_response( - '/v2/service_brokers/broker_id', - NO_CONTENT, - None) - self.client.v2.service_brokers.remove('broker_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - @patch.object(sys, 'argv', ['main', 'list_service_brokers']) - def test_main_list_service_brokers(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_brokers', - OK, - None, - 'v2', 'service_brokers', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_service_broker', 'ade9730c-4ee5-4290-ad37-0b15cecd2ca6']) - def test_main_get_service_broker(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_brokers/ade9730c-4ee5-4290-ad37-0b15cecd2ca6', - OK, - None, - 'v2', 'service_brokers', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v2/test_service_instances.py b/test/v2/test_service_instances.py deleted file mode 100644 index cf5dd0e..0000000 --- a/test/v2/test_service_instances.py +++ /dev/null @@ -1,208 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, call, CREATED, NO_CONTENT - - -class TestServiceInstances(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response( - '/v2/service_instances?q=service_plan_guid%3Aplan_id&q=space_guid%3Aspace_guid', - OK, - None, - 'v2', 'service_instances', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.service_instances.list(space_guid='space_guid', service_plan_guid='plan_id'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/service_instances/instance_id', - OK, - None, - 'v2', 'service_instances', 'GET_{id}_response.json') - result = self.client.v2.service_instances.get('instance_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_create(self): - self.client.post.return_value = mock_response( - '/v2/service_instances', - CREATED, - None, - 'v2', 'service_instances', 'POST_response.json') - service_instance = self.client.v2.service_instances.create('space_guid', 'name', 'plan_id', - parameters=dict( - the_service_broker="wants this object"), - tags=['mongodb'], - accepts_incomplete=True) - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(name='name', - space_guid='space_guid', - service_plan_guid='plan_id', - parameters=dict( - the_service_broker="wants this object"), - tags=['mongodb']), - params=dict(accepts_incomplete="true") - ) - self.assertIsNotNone(service_instance) - - def test_update(self): - self.client.put.return_value = mock_response( - '/v2/service_instances/instance_id', - OK, - None, - 'v2', 'service_instances', 'PUT_{id}_response.json') - service_instance = self.client.v2.service_instances.update('instance_id', instance_name='new-name', - tags=['other-tag']) - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(name='new-name', - tags=['other-tag']), params=None) - self.assertIsNotNone(service_instance) - - def test_update(self): - self.client.put.return_value = mock_response( - '/v2/service_instances/instance_id', - OK, - None, - 'v2', 'service_instances', 'PUT_{id}_response.json') - service_instance = self.client.v2.service_instances.update('instance_id', instance_name='new-name', - tags=['other-tag'], accepts_incomplete=True) - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(name='new-name', - tags=['other-tag']), params={'accepts_incomplete': 'true'}) - self.assertIsNotNone(service_instance) - - def test_delete_accepts_incomplete(self): - self.client.delete.return_value = mock_response( - '/v2/service_instances/instance_id', - NO_CONTENT, - None) - self.client.v2.service_instances.remove('instance_id', accepts_incomplete=True) - self.client.delete.assert_called_with(self.client.delete.return_value.url, - params=dict(accepts_incomplete="true")) - - self.client.delete.return_value = mock_response( - '/v2/service_instances/instance_id', - NO_CONTENT, - None) - self.client.v2.service_instances.remove('instance_id', accepts_incomplete="true") - self.client.delete.assert_called_with(self.client.delete.return_value.url, - params=dict(accepts_incomplete="true")) - - def test_delete_purge(self): - self.client.delete.return_value = mock_response( - '/v2/service_instances/instance_id', - NO_CONTENT, - None) - self.client.v2.service_instances.remove('instance_id', accepts_incomplete=True, purge=True) - self.client.delete.assert_called_with(self.client.delete.return_value.url, - params=dict(accepts_incomplete='true', purge="true")) - - self.client.delete.return_value = mock_response( - '/v2/service_instances/instance_id', - NO_CONTENT, - None) - self.client.v2.service_instances.remove('instance_id', purge="true") - self.client.delete.assert_called_with(self.client.delete.return_value.url, - params=dict(purge="true")) - - self.client.delete.return_value = mock_response( - '/v2/service_instances/instance_id', - NO_CONTENT, - None) - self.client.v2.service_instances.remove('instance_id', purge="true") - self.client.delete.assert_called_with(self.client.delete.return_value.url, - params=dict(purge="true")) - - def test_delete(self): - self.client.delete.return_value = mock_response( - '/v2/service_instances/instance_id', - NO_CONTENT, - None) - self.client.v2.service_instances.remove('instance_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url, - params={}) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/service_instances/instance_id', - OK, - None, - 'v2', 'service_instances', 'GET_{id}_response.json'), - mock_response( - '/v2/spaces/e3138257-8035-4c03-8aba-ab5d35eec0f9', - OK, - None, - 'v2', 'spaces', 'GET_{id}_response.json') - , - mock_response( - '/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96/service_bindings', - OK, - None, - 'v2', 'service_bindings', 'GET_response.json'), - mock_response( - '/v2/service_plans/65740f84-214a-46cf-b8e3-2233d580f293', - OK, - None, - 'v2', 'service_plans', 'GET_{id}_response.json'), - mock_response( - '/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96/routes', - OK, - None, - 'v2', 'routes', 'GET_response.json' - ), - mock_response( - '/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96/service_keys', - OK, - None, - 'v2', 'service_keys', 'GET_response.json' - ) - ] - service_instance = self.client.v2.service_instances.get('instance_id') - - self.assertIsNotNone(service_instance.space()) - cpt = reduce(lambda increment, _: increment + 1, service_instance.service_bindings(), 0) - self.assertEqual(cpt, 1) - self.assertIsNotNone(service_instance.service_plan()) - cpt = reduce(lambda increment, _: increment + 1, service_instance.routes(), 0) - self.assertEqual(cpt, 1) - cpt = reduce(lambda increment, _: increment + 1, service_instance.service_keys(), 0) - self.assertEqual(cpt, 1) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_service_instances']) - def test_main_list_service_instances(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_instances', - OK, - None, - 'v2', 'service_instances', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_service_instance', 'df52420f-d5b9-4b86-a7d3-6d7005d1ce96']) - def test_main_get_service_instance(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96', - OK, - None, - 'v2', 'service_instances', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v2/test_service_keys.py b/test/v2/test_service_keys.py deleted file mode 100644 index 734e22e..0000000 --- a/test/v2/test_service_keys.py +++ /dev/null @@ -1,111 +0,0 @@ -import json -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, CREATED, NO_CONTENT - - -class TestServiceKeys(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response( - '/v2/service_keys?q=service_instance_guid%3Ainstance_guid', - OK, - None, - 'v2', 'service_keys', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.service_keys.list(service_instance_guid='instance_guid'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/service_keys/key_id', - OK, - None, - 'v2', 'service_keys', 'GET_{id}_response.json') - result = self.client.v2.service_keys.get('key_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_create(self): - self.client.post.return_value = mock_response( - '/v2/service_keys', - CREATED, - None, - 'v2', 'service_keys', 'POST_response.json') - service_key = self.client.v2.service_keys.create('service_instance_id', 'name-127') - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(service_instance_guid='service_instance_id', - name='name-127') - ) - self.assertIsNotNone(service_key) - - def test_delete(self): - self.client.delete.return_value = mock_response( - '/v2/service_keys/key_id', - NO_CONTENT, - None) - self.client.v2.service_keys.remove('key_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - @patch.object(sys, 'argv', ['main', 'list_service_keys']) - def test_main_list_service_keys(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_keys', - OK, - None, - 'v2', 'service_keys', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_service_key', '67755c27-28ed-4087-9688-c07d92f3bcc9']) - def test_main_get_service_key(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_keys/67755c27-28ed-4087-9688-c07d92f3bcc9', - OK, - None, - 'v2', 'service_keys', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'create_service_key', json.dumps( - dict(service_instance_guid='service_instance_id', - name='name-127'))]) - def test_main_create_service_key(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.post.return_value = mock_response( - '/v2/service_keys', - CREATED, - None, - 'v2', 'service_keys', 'POST_response.json') - main.main() - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(service_instance_guid='service_instance_id', - name='name-127') - ) - - @patch.object(sys, 'argv', ['main', 'delete_service_key', '67755c27-28ed-4087-9688-c07d92f3bcc9']) - def test_main_delete_service_key(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.delete.return_value = mock_response( - '/v2/service_keys/67755c27-28ed-4087-9688-c07d92f3bcc9', - NO_CONTENT, - None) - main.main() - self.client.delete.assert_called_with(self.client.delete.return_value.url) - main.main() diff --git a/test/v2/test_service_plan_visibilities.py b/test/v2/test_service_plan_visibilities.py deleted file mode 100644 index 990bfca..0000000 --- a/test/v2/test_service_plan_visibilities.py +++ /dev/null @@ -1,125 +0,0 @@ -import json -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, CREATED, NO_CONTENT - - -class TestServicePlanVisibilities(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response( - '/v2/service_plan_visibilities?q=space_guid%3Aspace_guid', - OK, - None, - 'v2', 'service_plan_visibilities', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.service_plan_visibilities.list(space_guid='space_guid'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/service_plan_visibilities/guid', - OK, - None, - 'v2', 'service_plan_visibilities', 'GET_{id}_response.json') - result = self.client.v2.service_plan_visibilities.get('guid') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_create(self): - self.client.post.return_value = mock_response( - '/v2/service_plan_visibilities', - CREATED, - None, - 'v2', 'service_plan_visibilities', 'POST_response.json') - service_plan_visibilities = self.client.v2.service_plan_visibilities.create('service_plan_guid', - 'organization_guid') - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(service_plan_guid='service_plan_guid', - organization_guid='organization_guid')) - self.assertIsNotNone(service_plan_visibilities) - - def test_update(self): - self.client.put.return_value = mock_response( - '/v2/service_plan_visibilities/guid', - OK, - None, - 'v2', 'service_plan_visibilities', 'PUT_{id}_response.json') - service_plan_visibilities = \ - self.client.v2.service_plan_visibilities.update('guid', - service_plan_guid='service_plan_guid', - organization_guid='organization_guid') - self.client.put.assert_called_with(self.client.put.return_value.url, - json=dict(service_plan_guid='service_plan_guid', - organization_guid='organization_guid')) - self.assertIsNotNone(service_plan_visibilities) - - def test_delete(self): - self.client.delete.return_value = mock_response( - '/v2/service_plan_visibilities/guid', - NO_CONTENT, - None) - self.client.v2.service_plan_visibilities.remove('guid') - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - @patch.object(sys, 'argv', ['main', 'create_service_plan_visibility', json.dumps( - dict(service_plan_guid='service-plan-id', - organization_guid='organization-id'))]) - def test_main_create_service_plan_visibility(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.post.return_value = mock_response( - '/v2/service_plan_visibilities', - CREATED, - None, - 'v2', 'service_plan_visibilities', 'POST_response.json') - main.main() - self.client.post.assert_called_with(self.client.post.return_value.url, - json=dict(service_plan_guid='service-plan-id', - organization_guid='organization-id') - ) - - @patch.object(sys, 'argv', ['main', 'list_service_plan_visibilities']) - def test_main_list_service_plan_visibilities(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_plan_visibilities', - OK, - None, - 'v2', 'service_plan_visibilities', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_service_plan_visibility', 'a353104b-1290-418c-bc03-0e647afd0853']) - def test_main_get_service_plan_visibility(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response( - '/v2/service_plan_visibilities/a353104b-1290-418c-bc03-0e647afd0853', - OK, - None, - 'v2', 'service_plan_visibilities', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'delete_service_plan_visibility', '906775ea-622e-4bc7-af5d-9aab3b652f81']) - def test_main_delete_service_plan_visibility(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.delete.return_value = mock_response('/v2/service_plan_visibilities/906775ea-622e-4bc7-af5d-9aab3b652f81', - NO_CONTENT, - None) - main.main() - self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/test/v2/test_service_plans.py b/test/v2/test_service_plans.py deleted file mode 100644 index 827519b..0000000 --- a/test/v2/test_service_plans.py +++ /dev/null @@ -1,97 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, call - - -class TestServicePlans(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response( - '/v2/service_plans?q=service_guid%3Aservice_id', - OK, - None, - 'v2', 'service_plans', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, self.client.v2.service_plans.list(service_guid='service_id'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/service_plans/plan_id', - OK, - None, - 'v2', 'service_plans', 'GET_{id}_response.json') - result = self.client.v2.service_plans.get('plan_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_list_instances(self): - self.client.get.return_value = mock_response( - '/v2/service_plans/plan_id/service_instances?q=space_guid%3Aspace_id', - OK, - None, - 'v2', 'apps', 'GET_{id}_routes_response.json') - cpt = reduce(lambda increment, _: increment + 1, - self.client.v2.service_plans.list_instances('plan_id', space_guid='space_id'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/service_plans/plan_id', - OK, - None, - 'v2', 'service_plans', 'GET_{id}_response.json'), - mock_response( - '/v2/services/6a4abae6-93e0-438b-aaa2-5ae67f3a069d', - OK, - None, - 'v2', 'services', 'GET_{id}_response.json') - , - mock_response( - '/v2/service_plans/5d8f3b0f-6b5b-487f-8fed-4c2d9b812a72/service_instances', - OK, - None, - 'v2', 'service_instances', 'GET_response.json') - ] - service_plan = self.client.v2.service_plans.get('plan_id') - - self.assertIsNotNone(service_plan.service()) - cpt = reduce(lambda increment, _: increment + 1, service_plan.service_instances(), 0) - self.assertEqual(cpt, 1) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_service_plans']) - def test_main_list_service_plans(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_plans', - OK, - None, - 'v2', 'service_plans', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_service_plan', '5d8f3b0f-6b5b-487f-8fed-4c2d9b812a72']) - def test_main_get_service_plan(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/service_plans/5d8f3b0f-6b5b-487f-8fed-4c2d9b812a72', - OK, - None, - 'v2', 'service_plans', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v2/test_services.py b/test/v2/test_services.py deleted file mode 100644 index e1a4c58..0000000 --- a/test/v2/test_services.py +++ /dev/null @@ -1,78 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, call - - -class TestServices(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v2/services?q=label%3Asome_label', - OK, - None, - 'v2', 'services', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, self.client.v2.services.list(label='some_label'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/services/service_id', - OK, - None, - 'v2', 'services', 'GET_{id}_response.json') - result = self.client.v2.services.get('service_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/services/service_id', - OK, - None, - 'v2', 'services', 'GET_{id}_response.json'), - mock_response( - '/v2/services/2c883dbb-a726-4ecf-a0b7-d65588897e7f/service_plans', - OK, - None, - 'v2', 'service_plans', 'GET_response.json') - - ] - service = self.client.v2.services.get('service_id') - cpt = reduce(lambda increment, _: increment + 1, service.service_plans(), 0) - self.assertEqual(cpt, 1) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_services']) - def test_main_list_services(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/services', - OK, - None, - 'v2', 'services', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_service', '2c883dbb-a726-4ecf-a0b7-d65588897e7f']) - def test_main_get_service(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/services/2c883dbb-a726-4ecf-a0b7-d65588897e7f', - OK, - None, - 'v2', 'services', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v2/test_spaces.py b/test/v2/test_spaces.py deleted file mode 100644 index 80e8735..0000000 --- a/test/v2/test_spaces.py +++ /dev/null @@ -1,90 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK, reduce -from fake_requests import mock_response -from imported import patch, call - - -class TestSpaces(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v2/spaces?q=organization_guid%3Aorg_id', - OK, - None, - 'v2', 'spaces', 'GET_response.json') - cpt = reduce(lambda increment, _: increment + 1, self.client.v2.spaces.list(organization_guid='org_id'), 0) - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(cpt, 1) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v2/spaces/space_id', - OK, - None, - 'v2', 'spaces', 'GET_{id}_response.json') - result = self.client.v2.spaces.get('space_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_entity(self): - self.client.get.side_effect = [ - mock_response( - '/v2/spaces/space_id', - OK, - None, - 'v2', 'spaces', 'GET_{id}_response.json'), - mock_response( - '/v2/organizations/d7d77408-a250-45e3-8de5-71fcf199bbab', - OK, - None, - 'v2', 'organizations', 'GET_{id}_response.json'), - mock_response( - '/v2/spaces/2d745a4b-67e3-4398-986e-2adbcf8f7ec9/apps', - OK, - None, - 'v2', 'apps', 'GET_response.json'), - mock_response( - '/v2/spaces/2d745a4b-67e3-4398-986e-2adbcf8f7ec9/service_instances', - OK, - None, - 'v2', 'service_instances', 'GET_response.json') - ] - space = self.client.v2.spaces.get('space_id') - self.assertIsNotNone(space.organization()) - cpt = reduce(lambda increment, _: increment + 1, space.apps(), 0) - self.assertEqual(cpt, 3) - cpt = reduce(lambda increment, _: increment + 1, space.service_instances(), 0) - self.assertEqual(cpt, 1) - self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], - any_order=False) - - @patch.object(sys, 'argv', ['main', 'list_spaces']) - def test_main_list_spaces(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/spaces', - OK, - None, - 'v2', 'spaces', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_space', '2d745a4b-67e3-4398-986e-2adbcf8f7ec9']) - def test_main_get_spaces(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v2/spaces/2d745a4b-67e3-4398-986e-2adbcf8f7ec9', - OK, - None, - 'v2', 'spaces', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v3/test_apps.py b/test/v3/test_apps.py deleted file mode 100644 index 0048d53..0000000 --- a/test/v3/test_apps.py +++ /dev/null @@ -1,69 +0,0 @@ -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK -from cloudfoundry_client.v3.entities import Entity -from fake_requests import mock_response -from imported import call, NO_CONTENT - - -class TestApps(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v3/apps', - OK, - None, - 'v3', 'apps', 'GET_response.json') - all_applications = [application for application in self.client.v3.apps.list()] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(2, len(all_applications)) - self.assertEqual(all_applications[0]['name'], "my_app") - self.assertIsInstance(all_applications[0], Entity) - - def test_get(self): - self.client.get.return_value = mock_response('/v3/apps/app_id', - OK, - None, - 'v3', 'apps', 'GET_{id}_response.json') - application = self.client.v3.apps.get('app_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual("my_app", application['name']) - self.assertIsInstance(application, Entity) - - def test_get_then_space(self): - get_app = mock_response('/v3/apps/app_id', OK, None, 'v3', 'apps', 'GET_{id}_response.json') - get_space = mock_response('/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576', OK, None, - 'v3', 'spaces', 'GET_{id}_response.json') - self.client.get.side_effect = [ - get_app, - get_space - ] - space = self.client.v3.apps.get('app_id').space() - # self.client.get.assert_has_calls([call(get_app.url), - # call(get_space.url)], - # any_order=False) - self.assertEqual("my-space", space['name']) - - def test_get_then_start(self): - self.client.get.return_value = mock_response('/v3/apps/app_id', OK, None, - 'v3', 'apps', 'GET_{id}_response.json') - self.client.post.return_value = mock_response('/v3/apps/app_id/actions/start', OK, None, - 'v3', 'apps', 'POST_{id}_actions_start_response.json') - - app = self.client.v3.apps.get('app_id').start() - self.client.get.assert_called_with(self.client.get.return_value.url) - self.client.post.assert_called_with(self.client.post.return_value.url, files=None, json=None) - self.assertEqual("my_app", app['name']) - self.assertIsInstance(app, Entity) - - def test_remove(self): - self.client.delete.return_value = mock_response( - '/v3/apps/app_id', NO_CONTENT, None) - self.client.v3.apps.remove('app_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/test/v3/test_buildpacks.py b/test/v3/test_buildpacks.py deleted file mode 100644 index 7929ba1..0000000 --- a/test/v3/test_buildpacks.py +++ /dev/null @@ -1,93 +0,0 @@ -import sys -import unittest - -import cloudfoundry_client.main.main as main -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK -from cloudfoundry_client.v3.entities import Entity -from fake_requests import mock_response -from imported import CREATED, patch -from imported import call, NO_CONTENT - - -class TestBuildpacks(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v3/buildpacks', - OK, - None, - 'v3', 'buildpacks', 'GET_response.json') - all_buildpacks = [buildpack for buildpack in self.client.v3.buildpacks.list()] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(1, len(all_buildpacks)) - self.assertEqual(all_buildpacks[0]['name'], "my-buildpack") - self.assertIsInstance(all_buildpacks[0], Entity) - - def test_get(self): - self.client.get.return_value = mock_response( - '/v3/buildpacks/buildpack_id', - OK, - None, - 'v3', 'buildpacks', 'GET_{id}_response.json') - result = self.client.v3.buildpacks.get('buildpack_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertIsNotNone(result) - - def test_update(self): - self.client.patch.return_value = mock_response( - '/v3/buildpacks/buildpack_id', - OK, - None, - 'v3', 'buildpacks', 'PATCH_{id}_response.json') - result = self.client.v3.buildpacks.update('buildpack_id', 'ruby_buildpack', - enabled=True, - position=42, - stack='windows64') - self.client.patch.assert_called_with(self.client.patch.return_value.url, - json={'locked': False, - 'name': 'ruby_buildpack', - 'enabled': True, - 'position': 42, - 'stack': 'windows64', - 'metadata': { - 'labels': None, - 'annotations': None - } - }) - self.assertIsNotNone(result) - - def test_remove(self): - self.client.delete.return_value = mock_response( - '/v3/buildpacks/buildpack_id', - NO_CONTENT, - None) - self.client.v3.buildpacks.remove('buildpack_id') - self.client.delete.assert_called_with(self.client.delete.return_value.url) - - @patch.object(sys, 'argv', ['main', 'list_buildpacks']) - def test_main_list_buildpacks(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v3/buildpacks', - OK, - None, - 'v3', 'buildpacks', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'get_buildpack', '6e72c33b-dff0-4020-8603-bcd8a4eb05e4']) - def test_main_get_buildpack(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v3/buildpacks/6e72c33b-dff0-4020-8603-bcd8a4eb05e4', - OK, - None, - 'v3', 'buildpacks', 'GET_{id}_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/test/v3/test_organizations.py b/test/v3/test_organizations.py deleted file mode 100644 index 4665ab4..0000000 --- a/test/v3/test_organizations.py +++ /dev/null @@ -1,38 +0,0 @@ -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK -from cloudfoundry_client.v3.entities import Entity -from fake_requests import mock_response - - -class TestOrganizations(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v3/organizations', - OK, - None, - 'v3', 'organizations', 'GET_response.json') - all_organizations = [organization for organization in self.client.v3.organizations.list()] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(2, len(all_organizations)) - self.assertEqual(all_organizations[0]['name'], "org1") - self.assertIsInstance(all_organizations[0], Entity) - - def test_get(self): - self.client.get.return_value = mock_response('/v3/organizations/organization_id', - OK, - None, - 'v3', 'organizations', 'GET_{id}_response.json') - organization = self.client.v3.organizations.get('organization_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual("my-organization", organization['name']) - self.assertIsInstance(organization, Entity) - - diff --git a/test/v3/test_service_instances.py b/test/v3/test_service_instances.py deleted file mode 100644 index 4e1059a..0000000 --- a/test/v3/test_service_instances.py +++ /dev/null @@ -1,38 +0,0 @@ -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK -from cloudfoundry_client.v3.entities import Entity -from fake_requests import mock_response - - -class TestServiceInstances(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v3/service_instances', - OK, - None, - 'v3', 'service_instances', 'GET_response.json') - all_service_instances = [service_instance for service_instance in self.client.v3.service_instances.list()] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(1, len(all_service_instances)) - self.assertEqual(all_service_instances[0]['guid'], "85ccdcad-d725-4109-bca4-fd6ba062b5c8") - self.assertIsInstance(all_service_instances[0], Entity) - - def test_get(self): - self.client.get.return_value = mock_response('/v3/service_instances/service_instance_id', - OK, - None, - 'v3', 'service_instances', 'GET_{id}_response.json') - service_instance = self.client.v3.service_instances.get('service_instance_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual("85ccdcad-d725-4109-bca4-fd6ba062b5c8", service_instance['guid']) - self.assertIsInstance(service_instance, Entity) - - diff --git a/test/v3/test_spaces.py b/test/v3/test_spaces.py deleted file mode 100644 index bb07931..0000000 --- a/test/v3/test_spaces.py +++ /dev/null @@ -1,53 +0,0 @@ -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK -from cloudfoundry_client.v3.entities import Entity -from fake_requests import mock_response -from imported import call, NO_CONTENT - - -class TestSpaces(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v3/spaces', - OK, - None, - 'v3', 'spaces', 'GET_response.json') - all_spaces = [space for space in self.client.v3.spaces.list()] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(2, len(all_spaces)) - self.assertEqual(all_spaces[0]['name'], "space1") - self.assertIsInstance(all_spaces[0], Entity) - - def test_get(self): - self.client.get.return_value = mock_response('/v3/spaces/space_id', - OK, - None, - 'v3', 'spaces', 'GET_{id}_response.json') - space = self.client.v3.spaces.get('space_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual("my-space", space['name']) - self.assertIsInstance(space, Entity) - - def test_get_then_space(self): - get_space = mock_response('/v3/spaces/space_id', OK, None, - 'v3', 'spaces', 'GET_{id}_response.json') - get_organization = mock_response('/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133', OK, None, - 'v3', 'organizations', 'GET_{id}_response.json') - self.client.get.side_effect = [ - get_space, - get_organization - ] - organization = self.client.v3.spaces.get('space_id').organization() - self.client.get.assert_has_calls([call(get_space.url), - call(get_organization.url)], - any_order=False) - self.assertEqual("my-organization", organization['name']) - diff --git a/test/v3/test_tasks.py b/test/v3/test_tasks.py deleted file mode 100644 index e4fed9c..0000000 --- a/test/v3/test_tasks.py +++ /dev/null @@ -1,96 +0,0 @@ -import sys -import unittest - -from abstract_test_case import AbstractTestCase -from cloudfoundry_client.imported import OK -from cloudfoundry_client.main import main -from cloudfoundry_client.v3.entities import Entity -from fake_requests import mock_response -from imported import patch, CREATED, ACCEPTED - - -class TestTasks(unittest.TestCase, AbstractTestCase): - @classmethod - def setUpClass(cls): - cls.mock_client_class() - - def setUp(self): - self.build_client() - - def test_list(self): - self.client.get.return_value = mock_response('/v3/tasks', - OK, - None, - 'v3', 'tasks', 'GET_response.json') - all_tasks = [task for task in self.client.v3.tasks.list()] - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual(2, len(all_tasks)) - self.assertEqual(all_tasks[0]['name'], "hello") - self.assertIsInstance(all_tasks[0], Entity) - - def test_get(self): - self.client.get.return_value = mock_response('/v3/tasks/task_id', - OK, - None, - 'v3', 'tasks', 'GET_{id}_response.json') - task = self.client.v3.tasks.get('task_id') - self.client.get.assert_called_with(self.client.get.return_value.url) - self.assertEqual("migrate", task['name']) - self.assertIsInstance(task, Entity) - - def test_create(self): - self.client.post.return_value = mock_response( - '/v3/apps/app_guid/tasks', - CREATED, - None, - 'v3', 'tasks', 'POST_response.json') - task = self.client.v3.tasks.create('app_guid', command='rake db:migrate') - self.client.post.assert_called_with(self.client.post.return_value.url, - files=None, - json=dict(command='rake db:migrate')) - self.assertIsNotNone(task) - - def test_cancel(self): - self.client.post.return_value = mock_response( - '/v3/tasks/task_guid/actions/cancel', - ACCEPTED, - None, - 'v3', 'tasks', 'POST_{id}_actions_cancel_response.json') - task = self.client.v3.tasks.cancel('task_guid') - self.client.post.assert_called_with(self.client.post.return_value.url, files=None, json=None) - self.assertIsNotNone(task) - - @patch.object(sys, 'argv', ['main', 'list_tasks', '-names', 'task_name']) - def test_list_tasks(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.get.return_value = mock_response('/v3/tasks?names=task_name', - OK, - None, - 'v3', 'tasks', 'GET_response.json') - main.main() - self.client.get.assert_called_with(self.client.get.return_value.url) - - @patch.object(sys, 'argv', ['main', 'create_task', 'app_id', '{"command": "rake db:migrate", "name": "example"}']) - def test_create_task(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.post.return_value = mock_response('/v3/apps/app_id/tasks', - CREATED, - None, - 'v3', 'tasks', 'POST_response.json') - main.main() - self.client.post.assert_called_with(self.client.post.return_value.url, - files=None, - json=dict(command='rake db:migrate', name='example')) - - @patch.object(sys, 'argv', ['main', 'cancel_task', 'task_id']) - def test_cancel_task(self): - with patch('cloudfoundry_client.main.main.build_client_from_configuration', - new=lambda: self.client): - self.client.post.return_value = mock_response('/v3/tasks/task_id/actions/cancel', - CREATED, - None, - 'v3', 'tasks', 'POST_{id}_actions_cancel_response.json') - main.main() - self.client.post.assert_called_with(self.client.post.return_value.url, files=None, json=None) diff --git a/test/__init__.py b/tests/__init__.py similarity index 88% rename from test/__init__.py rename to tests/__init__.py index 247398a..811245e 100644 --- a/test/__init__.py +++ b/tests/__init__.py @@ -1,9 +1,8 @@ #!/usr/bin/env python -import os -import sys import unittest + def get_suite(): loader = unittest.TestLoader() suite = loader.loadTestsFromModule(__package__) diff --git a/tests/abstract_test_case.py b/tests/abstract_test_case.py new file mode 100644 index 0000000..46ec190 --- /dev/null +++ b/tests/abstract_test_case.py @@ -0,0 +1,98 @@ +import json +import os +from http import HTTPStatus +from unittest.mock import MagicMock, patch + +from oauth2_client.credentials_manager import CredentialManager + +from cloudfoundry_client.client import CloudFoundryClient +from fake_requests import MockResponse + + +def mock_cloudfoundry_client_class(): + if not getattr(CloudFoundryClient, "CLASS_MOCKED", False): + mocked_attributes = ["get", "post", "patch", "put", "delete"] + + class MockClass(CredentialManager): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for attribute in mocked_attributes: + setattr(self, attribute, MagicMock()) + + CloudFoundryClient.__bases__ = (MockClass,) + setattr(CloudFoundryClient, "CLASS_MOCKED", True) + + +class AbstractTestCase(object): + TARGET_ENDPOINT = "http://somewhere.org" + AUTHORIZATION_ENDPOINT = "http://login.somewhere.org" + TOKEN_ENDPOINT = "http://token.somewhere.org" + DOPPLER_ENDPOINT = "wss://doppler.nd-cfapi.itn.ftgroup:443" + LOG_STREAM_ENDPOINT = "https://log-stream.nd-cfapi.itn.ftgroup" + + @classmethod + def mock_client_class(cls): + mock_cloudfoundry_client_class() + + def build_client(self): + with patch("cloudfoundry_client.client.requests") as fake_requests: + self._mock_info_calls(fake_requests) + self.client = CloudFoundryClient(self.TARGET_ENDPOINT) + + @staticmethod + def _mock_info_calls( + requests, + with_doppler: bool = True, + with_log_streams: bool = True, + with_v2: bool = True, + with_v3: bool = True + ): + links = { + "self": dict(href=AbstractTestCase.TARGET_ENDPOINT), + "cloud_controller_v2": dict( + href="%s/v2" % AbstractTestCase.TARGET_ENDPOINT, + meta=dict(version="2.141.0"), + ), + "cloud_controller_v3": dict( + href="%s/v3" % AbstractTestCase.TARGET_ENDPOINT, + meta=dict(version="3.76.0"), + ), + "logging": dict(href=AbstractTestCase.DOPPLER_ENDPOINT) if with_doppler else None, + "log_stream": dict(href=AbstractTestCase.LOG_STREAM_ENDPOINT) if with_log_streams else None, + "app_ssh": dict(href="ssh.nd-cfapi.itn.ftgroup:80"), + "uaa": dict(href="https://uaa.nd-cfapi.itn.ftgroup"), + "login": dict(href=AbstractTestCase.AUTHORIZATION_ENDPOINT), + "network_policy_v0": dict(href="https://api.nd-cfapi.itn.ftgroup/networking/v0/external"), + "network_policy_v1": dict(href="https://api.nd-cfapi.itn.ftgroup/networking/v1/external"), + } + if not with_v2: + del links["cloud_controller_v2"] + if not with_v3: + del links["cloud_controller_v3"] + requests.get.side_effect = [ + MockResponse( + "%s/" % AbstractTestCase.TARGET_ENDPOINT, + status_code=HTTPStatus.OK.value, + text=json.dumps(dict(links=links)), + ), + ] + + @staticmethod + def get_fixtures_path(*paths): + return os.path.join(os.path.dirname(__file__), "fixtures", *paths) + + @staticmethod + def mock_response(uri: str, status_code: HTTPStatus, headers: dict | None, *path_parts: str): + if len(path_parts) > 0: + file_name = path_parts[len(path_parts) - 1] + extension_idx = file_name.rfind(".") + binary_file = extension_idx >= 0 and file_name[extension_idx:] == ".bin" + with (open(AbstractTestCase.get_fixtures_path(*path_parts), "rb" if binary_file else "r")) as f: + return MockResponse( + url="%s%s" % (AbstractTestCase.TARGET_ENDPOINT, uri), + status_code=status_code.value, + text=f.read(), + headers=headers, + ) + else: + return MockResponse("%s%s" % (AbstractTestCase.TARGET_ENDPOINT, uri), status_code.value, "", headers) diff --git a/tests/fake_requests.py b/tests/fake_requests.py new file mode 100644 index 0000000..c0469b4 --- /dev/null +++ b/tests/fake_requests.py @@ -0,0 +1,45 @@ +from http import HTTPStatus +from json import loads +from unittest.mock import MagicMock + + +def iterate_text(text): + for character in text: + yield bytes([character]) + + +class MockSession(object): + def __init__(self): + self.headers = dict() + self.proxies = None + self.verify = True + self.trust_env = False + + +class MockResponse(object): + def __init__(self, url: str, status_code: int, text: str, headers: dict = None): + self.status_code = status_code + self.url = url + self.text = text + self.headers = dict() + self.is_redirect = status_code == HTTPStatus.SEE_OTHER + if headers is not None: + self.headers.update(headers) + + def check_data(self, data, json, **kwargs): + pass + + def json(self, **kwargs): + return loads(self.text, **kwargs) + + def __iter__(self): + return iterate_text(self.text) + + +class FakeRequests(object): + def __init__(self): + self.Session = MagicMock() + self.post = MagicMock() + self.get = MagicMock() + self.put = MagicMock() + self.patch = MagicMock() diff --git a/tests/fixtures/networking/v1/external/policies/GET_response.json b/tests/fixtures/networking/v1/external/policies/GET_response.json new file mode 100644 index 0000000..30f820c --- /dev/null +++ b/tests/fixtures/networking/v1/external/policies/GET_response.json @@ -0,0 +1,31 @@ +{ + "total_policies": 2, + "policies": [ + { + "source": { + "id": "1081ceac-f5c4-47a8-95e8-88e1e302efb5" + }, + "destination": { + "id": "38f08df0-19df-4439-b4e9-61096d4301ea", + "protocol": "tcp", + "ports": { + "start": 1234, + "end": 1235 + } + } + }, + { + "source": { + "id": "308e7ef1-63f1-4a6c-978c-2e527cbb1c36" + }, + "destination": { + "id": "308e7ef1-63f1-4a6c-978c-2e527cbb1c36", + "protocol": "tcp", + "ports": { + "start": 1234, + "end": 1235 + } + } + } + ] +} \ No newline at end of file diff --git a/test/fixtures/operations/manifest.yml b/tests/fixtures/operations/manifest.yml similarity index 70% rename from test/fixtures/operations/manifest.yml rename to tests/fixtures/operations/manifest.yml index 7556413..e6434de 100644 --- a/test/fixtures/operations/manifest.yml +++ b/tests/fixtures/operations/manifest.yml @@ -1,8 +1,8 @@ applications: - name: the-name routes: - - route: first-route - - route: second-route + - route: first-route + - route: second-route docker: username: the-user password: P@SsW0r$ diff --git a/test/fixtures/operations/manifest_complex.yml b/tests/fixtures/operations/manifest_complex.yml similarity index 100% rename from test/fixtures/operations/manifest_complex.yml rename to tests/fixtures/operations/manifest_complex.yml diff --git a/test/fixtures/operations/manifest_empty.yml b/tests/fixtures/operations/manifest_empty.yml similarity index 100% rename from test/fixtures/operations/manifest_empty.yml rename to tests/fixtures/operations/manifest_empty.yml diff --git a/tests/fixtures/operations/manifest_main.yml b/tests/fixtures/operations/manifest_main.yml new file mode 100644 index 0000000..710d377 --- /dev/null +++ b/tests/fixtures/operations/manifest_main.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: test_app + memory: 1024M + instances: 1 + path: . + health-check-type: http + health-check-http-endpoint: /manage/health + services: + - service1 + - service2 + no-route: true + env: + VAR1: VALUE1 + VAR2: VALUE2 + diff --git a/test/fixtures/recents/GET_response.bin b/tests/fixtures/recents/GET_response.bin similarity index 100% rename from test/fixtures/recents/GET_response.bin rename to tests/fixtures/recents/GET_response.bin diff --git a/test/fixtures/v2/apps/GET_response.json b/tests/fixtures/v2/apps/GET_response.json similarity index 100% rename from test/fixtures/v2/apps/GET_response.json rename to tests/fixtures/v2/apps/GET_response.json diff --git a/test/fixtures/v2/apps/GET_space_guid_name_response.json b/tests/fixtures/v2/apps/GET_space_guid_name_response.json similarity index 100% rename from test/fixtures/v2/apps/GET_space_guid_name_response.json rename to tests/fixtures/v2/apps/GET_space_guid_name_response.json diff --git a/test/fixtures/v2/apps/GET_{id}_env_response.json b/tests/fixtures/v2/apps/GET_{id}_env_response.json similarity index 99% rename from test/fixtures/v2/apps/GET_{id}_env_response.json rename to tests/fixtures/v2/apps/GET_{id}_env_response.json index 674dbc7..6f8f5aa 100644 --- a/test/fixtures/v2/apps/GET_{id}_env_response.json +++ b/tests/fixtures/v2/apps/GET_{id}_env_response.json @@ -10,7 +10,6 @@ }, "system_env_json": { "VCAP_SERVICES": { - } }, "application_env_json": { @@ -24,14 +23,12 @@ "application_version": "45919b1d-422d-4953-a0a3-91d7fd2899bf", "application_name": "name-243", "application_uris": [ - ], "version": "45919b1d-422d-4953-a0a3-91d7fd2899bf", "name": "name-243", "space_name": "name-244", "space_id": "885c392a-3502-4cff-a47d-49089b13b1b7", "uris": [ - ], "users": null } diff --git a/test/fixtures/v2/apps/GET_{id}_instances_response.json b/tests/fixtures/v2/apps/GET_{id}_instances_response.json similarity index 100% rename from test/fixtures/v2/apps/GET_{id}_instances_response.json rename to tests/fixtures/v2/apps/GET_{id}_instances_response.json diff --git a/test/fixtures/v2/apps/GET_{id}_instances_stopped_response.json b/tests/fixtures/v2/apps/GET_{id}_instances_stopped_response.json similarity index 100% rename from test/fixtures/v2/apps/GET_{id}_instances_stopped_response.json rename to tests/fixtures/v2/apps/GET_{id}_instances_stopped_response.json diff --git a/test/fixtures/v2/apps/GET_{id}_response.json b/tests/fixtures/v2/apps/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/apps/GET_{id}_response.json rename to tests/fixtures/v2/apps/GET_{id}_response.json diff --git a/test/fixtures/v2/apps/GET_{id}_routes_response.json b/tests/fixtures/v2/apps/GET_{id}_routes_response.json similarity index 100% rename from test/fixtures/v2/apps/GET_{id}_routes_response.json rename to tests/fixtures/v2/apps/GET_{id}_routes_response.json diff --git a/test/fixtures/v2/apps/GET_{id}_service_bindings_response.json b/tests/fixtures/v2/apps/GET_{id}_service_bindings_response.json similarity index 99% rename from test/fixtures/v2/apps/GET_{id}_service_bindings_response.json rename to tests/fixtures/v2/apps/GET_{id}_service_bindings_response.json index 1f1e0e9..5b13acc 100644 --- a/test/fixtures/v2/apps/GET_{id}_service_bindings_response.json +++ b/tests/fixtures/v2/apps/GET_{id}_service_bindings_response.json @@ -18,7 +18,6 @@ "creds-key-384": "creds-val-384" }, "binding_options": { - }, "gateway_data": null, "gateway_name": "", diff --git a/test/fixtures/v2/apps/GET_{id}_stats_response.json b/tests/fixtures/v2/apps/GET_{id}_stats_response.json similarity index 100% rename from test/fixtures/v2/apps/GET_{id}_stats_response.json rename to tests/fixtures/v2/apps/GET_{id}_stats_response.json diff --git a/tests/fixtures/v2/apps/GET_{id}_summary_response.json b/tests/fixtures/v2/apps/GET_{id}_summary_response.json new file mode 100644 index 0000000..91ba54f --- /dev/null +++ b/tests/fixtures/v2/apps/GET_{id}_summary_response.json @@ -0,0 +1,59 @@ +{ + "version": "0b9e28b8-b69d-46ba-ad7e-6c83899826b2", + "staging_failed_reason": null, + "docker_credentials_json": { + "redacted_message": "[PRIVATE DATA HIDDEN]" + }, + "staging_failed_description": null, + "instances": 1, + "guid": "a5eee659-56a7-4123-91ac-ba190ddc5477", + "docker_image": null, + "diego": true, + "console": false, + "package_state": "STAGED", + "state": "STOPPED", + "production": false, + "stack_guid": "3cb62b55-3230-45d1-a297-ace25b3e3479", + "memory": 512, + "package_updated_at": "2016-08-10T14:01:54Z", + "staging_task_id": "c80457643d7d455c93f92151d6384c2d", + "buildpack": null, + "enable_ssh": true, + "detected_start_command": "sh boot.sh", + "disk_quota": 1024, + "routes": [ + { + "path": "", + "host": "application_name", + "guid": "6c7d07f9-57a1-474e-8e91-82a597550956", + "port": null, + "domain": { + "guid": "f030f4bb-2b60-47e9-be3e-8105655c0286", + "name": "some-domain" + } + } + ], + "services": [], + "detected_buildpack": "staticfile 1.3.8", + "space_guid": "6c99652b-4795-416c-9466-64a3e4555627", + "name": "application_name", + "running_instances": 0, + "health_check_type": "port", + "command": null, + "debug": null, + "available_domains": [ + { + "guid": "255b9370-3836-402c-9e1f-08b2a96dd180", + "name": "some-domain", + "owning_organization_guid": "d7d77408-a250-45e3-8de5-71fcf199bbab" + }, + { + "guid": "f030f4bb-2b60-47e9-be3e-8105655c0286", + "name": "other-domain", + "owning_organization_guid": "d7d77408-a250-45e3-8de5-71fcf199bbab" + } + ], + "environment_json": {}, + "ports": null, + "health_check_timeout": null +} \ No newline at end of file diff --git a/test/fixtures/v2/apps/POST_response.json b/tests/fixtures/v2/apps/POST_response.json similarity index 100% rename from test/fixtures/v2/apps/POST_response.json rename to tests/fixtures/v2/apps/POST_response.json diff --git a/test/fixtures/v2/apps/POST_{id}_restage_response.json b/tests/fixtures/v2/apps/POST_{id}_restage_response.json similarity index 100% rename from test/fixtures/v2/apps/POST_{id}_restage_response.json rename to tests/fixtures/v2/apps/POST_{id}_restage_response.json diff --git a/test/fixtures/v2/apps/PUT_{id}_response.json b/tests/fixtures/v2/apps/PUT_{id}_response.json similarity index 100% rename from test/fixtures/v2/apps/PUT_{id}_response.json rename to tests/fixtures/v2/apps/PUT_{id}_response.json diff --git a/test/fixtures/v2/apps/PUT_{id}_routes_{route_id}_response.json b/tests/fixtures/v2/apps/PUT_{id}_routes_{route_id}_response.json similarity index 100% rename from test/fixtures/v2/apps/PUT_{id}_routes_{route_id}_response.json rename to tests/fixtures/v2/apps/PUT_{id}_routes_{route_id}_response.json diff --git a/test/fixtures/v2/buildpacks/GET_response.json b/tests/fixtures/v2/buildpacks/GET_response.json similarity index 100% rename from test/fixtures/v2/buildpacks/GET_response.json rename to tests/fixtures/v2/buildpacks/GET_response.json diff --git a/test/fixtures/v2/buildpacks/GET_{id}_response.json b/tests/fixtures/v2/buildpacks/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/buildpacks/GET_{id}_response.json rename to tests/fixtures/v2/buildpacks/GET_{id}_response.json diff --git a/test/fixtures/v2/buildpacks/PUT_{id}_response.json b/tests/fixtures/v2/buildpacks/PUT_{id}_response.json similarity index 100% rename from test/fixtures/v2/buildpacks/PUT_{id}_response.json rename to tests/fixtures/v2/buildpacks/PUT_{id}_response.json diff --git a/test/fixtures/v2/events/GET_response_audit.route.delete-request.json b/tests/fixtures/v2/events/GET_response_audit.route.delete-request.json similarity index 100% rename from test/fixtures/v2/events/GET_response_audit.route.delete-request.json rename to tests/fixtures/v2/events/GET_response_audit.route.delete-request.json diff --git a/tests/fixtures/v2/fake/GET_invalid_entity_with_invalid_entity_type.json b/tests/fixtures/v2/fake/GET_invalid_entity_with_invalid_entity_type.json new file mode 100644 index 0000000..1269121 --- /dev/null +++ b/tests/fixtures/v2/fake/GET_invalid_entity_with_invalid_entity_type.json @@ -0,0 +1,9 @@ +{ + "metadata": { + "guid": "any-id", + "url": "/fake/any-id", + "created_at": "2015-11-30T23:38:33Z", + "updated_at": "2015-11-30T23:38:33Z" + }, + "entity": [] +} \ No newline at end of file diff --git a/tests/fixtures/v2/fake/GET_invalid_entity_with_null_entity.json b/tests/fixtures/v2/fake/GET_invalid_entity_with_null_entity.json new file mode 100644 index 0000000..186e6b9 --- /dev/null +++ b/tests/fixtures/v2/fake/GET_invalid_entity_with_null_entity.json @@ -0,0 +1,9 @@ +{ + "metadata": { + "guid": "any-id", + "url": "/fake/any-id", + "created_at": "2015-11-30T23:38:33Z", + "updated_at": "2015-11-30T23:38:33Z" + }, + "entity": null +} \ No newline at end of file diff --git a/tests/fixtures/v2/fake/GET_invalid_entity_without_entity.json b/tests/fixtures/v2/fake/GET_invalid_entity_without_entity.json new file mode 100644 index 0000000..d977346 --- /dev/null +++ b/tests/fixtures/v2/fake/GET_invalid_entity_without_entity.json @@ -0,0 +1,8 @@ +{ + "metadata": { + "guid": "any-id", + "url": "/fake/any-id", + "created_at": "2015-11-30T23:38:33Z", + "updated_at": "2015-11-30T23:38:33Z" + } +} \ No newline at end of file diff --git a/test/fixtures/fake/GET_multi_page_0_response.json b/tests/fixtures/v2/fake/GET_multi_page_0_response.json similarity index 100% rename from test/fixtures/fake/GET_multi_page_0_response.json rename to tests/fixtures/v2/fake/GET_multi_page_0_response.json diff --git a/test/fixtures/fake/GET_multi_page_1_response.json b/tests/fixtures/v2/fake/GET_multi_page_1_response.json similarity index 100% rename from test/fixtures/fake/GET_multi_page_1_response.json rename to tests/fixtures/v2/fake/GET_multi_page_1_response.json diff --git a/test/fixtures/fake/GET_response.json b/tests/fixtures/v2/fake/GET_response.json similarity index 100% rename from test/fixtures/fake/GET_response.json rename to tests/fixtures/v2/fake/GET_response.json diff --git a/test/fixtures/fake/GET_{id}_response.json b/tests/fixtures/v2/fake/GET_{id}_response.json similarity index 100% rename from test/fixtures/fake/GET_{id}_response.json rename to tests/fixtures/v2/fake/GET_{id}_response.json diff --git a/test/fixtures/v2/organizations/GET_response.json b/tests/fixtures/v2/organizations/GET_response.json similarity index 100% rename from test/fixtures/v2/organizations/GET_response.json rename to tests/fixtures/v2/organizations/GET_response.json diff --git a/test/fixtures/v2/organizations/GET_{id}_response.json b/tests/fixtures/v2/organizations/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/organizations/GET_{id}_response.json rename to tests/fixtures/v2/organizations/GET_{id}_response.json diff --git a/test/fixtures/v2/routes/GET_response.json b/tests/fixtures/v2/routes/GET_response.json similarity index 100% rename from test/fixtures/v2/routes/GET_response.json rename to tests/fixtures/v2/routes/GET_response.json diff --git a/test/fixtures/v2/routes/GET_{id}_response.json b/tests/fixtures/v2/routes/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/routes/GET_{id}_response.json rename to tests/fixtures/v2/routes/GET_{id}_response.json diff --git a/test/fixtures/v2/service_bindings/GET_response.json b/tests/fixtures/v2/service_bindings/GET_response.json similarity index 99% rename from test/fixtures/v2/service_bindings/GET_response.json rename to tests/fixtures/v2/service_bindings/GET_response.json index 1f1e0e9..5b13acc 100644 --- a/test/fixtures/v2/service_bindings/GET_response.json +++ b/tests/fixtures/v2/service_bindings/GET_response.json @@ -18,7 +18,6 @@ "creds-key-384": "creds-val-384" }, "binding_options": { - }, "gateway_data": null, "gateway_name": "", diff --git a/test/fixtures/v2/service_bindings/GET_{id}_response.json b/tests/fixtures/v2/service_bindings/GET_{id}_response.json similarity index 99% rename from test/fixtures/v2/service_bindings/GET_{id}_response.json rename to tests/fixtures/v2/service_bindings/GET_{id}_response.json index b34c620..17d034b 100644 --- a/test/fixtures/v2/service_bindings/GET_{id}_response.json +++ b/tests/fixtures/v2/service_bindings/GET_{id}_response.json @@ -12,7 +12,6 @@ "creds-key-376": "creds-val-376" }, "binding_options": { - }, "gateway_data": null, "gateway_name": "", diff --git a/test/fixtures/v2/service_bindings/POST_response.json b/tests/fixtures/v2/service_bindings/POST_response.json similarity index 99% rename from test/fixtures/v2/service_bindings/POST_response.json rename to tests/fixtures/v2/service_bindings/POST_response.json index 4b19ead..9500b9f 100644 --- a/test/fixtures/v2/service_bindings/POST_response.json +++ b/tests/fixtures/v2/service_bindings/POST_response.json @@ -12,7 +12,6 @@ "creds-key-390": "creds-val-390" }, "binding_options": { - }, "gateway_data": null, "gateway_name": "", diff --git a/test/fixtures/v2/service_brokers/GET_response.json b/tests/fixtures/v2/service_brokers/GET_response.json similarity index 100% rename from test/fixtures/v2/service_brokers/GET_response.json rename to tests/fixtures/v2/service_brokers/GET_response.json diff --git a/test/fixtures/v2/service_brokers/GET_{id}_response.json b/tests/fixtures/v2/service_brokers/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/service_brokers/GET_{id}_response.json rename to tests/fixtures/v2/service_brokers/GET_{id}_response.json diff --git a/test/fixtures/v2/service_brokers/POST_response.json b/tests/fixtures/v2/service_brokers/POST_response.json similarity index 100% rename from test/fixtures/v2/service_brokers/POST_response.json rename to tests/fixtures/v2/service_brokers/POST_response.json diff --git a/test/fixtures/v2/service_brokers/PUT_{id}_response.json b/tests/fixtures/v2/service_brokers/PUT_{id}_response.json similarity index 100% rename from test/fixtures/v2/service_brokers/PUT_{id}_response.json rename to tests/fixtures/v2/service_brokers/PUT_{id}_response.json diff --git a/test/fixtures/v2/service_instances/GET_response.json b/tests/fixtures/v2/service_instances/GET_response.json similarity index 100% rename from test/fixtures/v2/service_instances/GET_response.json rename to tests/fixtures/v2/service_instances/GET_response.json diff --git a/test/fixtures/v2/service_instances/GET_{id}_response.json b/tests/fixtures/v2/service_instances/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/service_instances/GET_{id}_response.json rename to tests/fixtures/v2/service_instances/GET_{id}_response.json diff --git a/test/fixtures/v2/service_instances/POST_response.json b/tests/fixtures/v2/service_instances/POST_response.json similarity index 99% rename from test/fixtures/v2/service_instances/POST_response.json rename to tests/fixtures/v2/service_instances/POST_response.json index 62428f3..f990438 100644 --- a/test/fixtures/v2/service_instances/POST_response.json +++ b/tests/fixtures/v2/service_instances/POST_response.json @@ -8,7 +8,6 @@ "entity": { "name": "test_name", "credentials": { - }, "service_plan_guid": "ce1aeaa9-c5ef-417e-abf7-b16f07724aac", "space_guid": "2d745a4b-67e3-4398-986e-2adbcf8f7ec9", @@ -23,7 +22,6 @@ "created_at": "2016-07-23T12:04:33Z" }, "tags": [ - ], "space_url": "/v2/spaces/2d745a4b-67e3-4398-986e-2adbcf8f7ec9", "service_plan_url": "/v2/service_plans/ce1aeaa9-c5ef-417e-abf7-b16f07724aac", diff --git a/test/fixtures/v2/service_instances/PUT_{id}_response.json b/tests/fixtures/v2/service_instances/PUT_{id}_response.json similarity index 99% rename from test/fixtures/v2/service_instances/PUT_{id}_response.json rename to tests/fixtures/v2/service_instances/PUT_{id}_response.json index cece18f..99f5624 100644 --- a/test/fixtures/v2/service_instances/PUT_{id}_response.json +++ b/tests/fixtures/v2/service_instances/PUT_{id}_response.json @@ -23,7 +23,6 @@ "created_at": "2015-11-30T23:39:01Z" }, "tags": [ - ], "space_url": "/v2/spaces/3a46f977-c962-4445-8086-c3410a38dd2f", "service_plan_url": "/v2/service_plans/cf23d3e3-5f83-45db-be33-3a6a13203b43", diff --git a/test/fixtures/v2/service_keys/GET_response.json b/tests/fixtures/v2/service_keys/GET_response.json similarity index 100% rename from test/fixtures/v2/service_keys/GET_response.json rename to tests/fixtures/v2/service_keys/GET_response.json diff --git a/test/fixtures/v2/service_keys/GET_{id}_response.json b/tests/fixtures/v2/service_keys/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/service_keys/GET_{id}_response.json rename to tests/fixtures/v2/service_keys/GET_{id}_response.json diff --git a/test/fixtures/v2/service_keys/POST_response.json b/tests/fixtures/v2/service_keys/POST_response.json similarity index 100% rename from test/fixtures/v2/service_keys/POST_response.json rename to tests/fixtures/v2/service_keys/POST_response.json diff --git a/test/fixtures/v2/service_plan_visibilities/GET_response.json b/tests/fixtures/v2/service_plan_visibilities/GET_response.json similarity index 100% rename from test/fixtures/v2/service_plan_visibilities/GET_response.json rename to tests/fixtures/v2/service_plan_visibilities/GET_response.json diff --git a/test/fixtures/v2/service_plan_visibilities/GET_{id}_response.json b/tests/fixtures/v2/service_plan_visibilities/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/service_plan_visibilities/GET_{id}_response.json rename to tests/fixtures/v2/service_plan_visibilities/GET_{id}_response.json diff --git a/test/fixtures/v2/service_plan_visibilities/POST_response.json b/tests/fixtures/v2/service_plan_visibilities/POST_response.json similarity index 100% rename from test/fixtures/v2/service_plan_visibilities/POST_response.json rename to tests/fixtures/v2/service_plan_visibilities/POST_response.json diff --git a/test/fixtures/v2/service_plan_visibilities/PUT_{id}_response.json b/tests/fixtures/v2/service_plan_visibilities/PUT_{id}_response.json similarity index 100% rename from test/fixtures/v2/service_plan_visibilities/PUT_{id}_response.json rename to tests/fixtures/v2/service_plan_visibilities/PUT_{id}_response.json diff --git a/test/fixtures/v2/service_plans/GET_response.json b/tests/fixtures/v2/service_plans/GET_response.json similarity index 100% rename from test/fixtures/v2/service_plans/GET_response.json rename to tests/fixtures/v2/service_plans/GET_response.json diff --git a/test/fixtures/v2/service_plans/GET_{id}_response.json b/tests/fixtures/v2/service_plans/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/service_plans/GET_{id}_response.json rename to tests/fixtures/v2/service_plans/GET_{id}_response.json diff --git a/test/fixtures/v2/services/GET_response.json b/tests/fixtures/v2/services/GET_response.json similarity index 100% rename from test/fixtures/v2/services/GET_response.json rename to tests/fixtures/v2/services/GET_response.json diff --git a/test/fixtures/v2/services/GET_{id}_response.json b/tests/fixtures/v2/services/GET_{id}_response.json similarity index 99% rename from test/fixtures/v2/services/GET_{id}_response.json rename to tests/fixtures/v2/services/GET_{id}_response.json index 3bdbcfd..a2da462 100644 --- a/test/fixtures/v2/services/GET_{id}_response.json +++ b/tests/fixtures/v2/services/GET_{id}_response.json @@ -18,10 +18,8 @@ "unique_id": "b34d6452-7ee5-40c0-b089-71357ba760e7", "extra": null, "tags": [ - ], "requires": [ - ], "documentation_url": null, "service_broker_guid": "68e0a454-d42c-410e-b566-9f7806fe87aa", diff --git a/test/fixtures/v2/spaces/GET_response.json b/tests/fixtures/v2/spaces/GET_response.json similarity index 100% rename from test/fixtures/v2/spaces/GET_response.json rename to tests/fixtures/v2/spaces/GET_response.json diff --git a/test/fixtures/v2/spaces/GET_{id}_response.json b/tests/fixtures/v2/spaces/GET_{id}_response.json similarity index 100% rename from test/fixtures/v2/spaces/GET_{id}_response.json rename to tests/fixtures/v2/spaces/GET_{id}_response.json diff --git a/test/fixtures/v3/apps/GET_response.json b/tests/fixtures/v3/apps/GET_response.json similarity index 98% rename from test/fixtures/v3/apps/GET_response.json rename to tests/fixtures/v3/apps/GET_response.json index d07436c..faec509 100644 --- a/test/fixtures/v3/apps/GET_response.json +++ b/tests/fixtures/v3/apps/GET_response.json @@ -84,7 +84,9 @@ "lifecycle": { "type": "buildpack", "data": { - "buildpacks": ["ruby_buildpack"], + "buildpacks": [ + "ruby_buildpack" + ], "stack": "cflinuxfs2" } }, diff --git a/tests/fixtures/v3/apps/GET_response_include_space.json b/tests/fixtures/v3/apps/GET_response_include_space.json new file mode 100644 index 0000000..6f50667 --- /dev/null +++ b/tests/fixtures/v3/apps/GET_response_include_space.json @@ -0,0 +1,173 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "http://somewhere.org/v3/apps?page=1&per_page=2" + }, + "last": { + "href": "http://somewhere.org/v3/apps?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446", + "name": "my_app", + "state": "STARTED", + "created_at": "2016-03-17T21:41:30Z", + "updated_at": "2016-03-18T11:32:30Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": [ + "java_buildpack" + ], + "stack": "cflinuxfs2" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446" + }, + "space": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/processes" + }, + "route_mappings": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/route_mappings" + }, + "packages": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/packages" + }, + "environment_variables": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/environment_variables" + }, + "current_droplet": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets/current" + }, + "droplets": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets" + }, + "tasks": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/tasks" + }, + "start": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/start", + "method": "POST" + }, + "stop": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/stop", + "method": "POST" + } + }, + "metadata": { + "labels": {} + } + }, + { + "guid": "02b4ec9b-94c7-4468-9c23-4e906191a0f8", + "name": "my_app2", + "state": "STOPPED", + "created_at": "1970-01-01T00:00:02Z", + "updated_at": "2016-06-08T16:41:26Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": [ + "ruby_buildpack" + ], + "stack": "cflinuxfs2" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8" + }, + "space": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/processes" + }, + "route_mappings": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/route_mappings" + }, + "packages": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/packages" + }, + "environment_variables": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/environment_variables" + }, + "current_droplet": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets/current" + }, + "droplets": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets" + }, + "tasks": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/tasks" + }, + "start": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/start", + "method": "POST" + }, + "stop": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/stop", + "method": "POST" + } + }, + "metadata": { + "labels": {} + } + } + ], + "included": { + "spaces": [ + { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my_space", + "relationships": { + "organization": { + "data": { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "quota": { + "data": null + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "organization": { + "href": "http://somewhere.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {} + } + } + ] + } +} 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}_droplets_response.json b/tests/fixtures/v3/apps/GET_{id}_droplets_response.json new file mode 100644 index 0000000..578a68b --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_droplets_response.json @@ -0,0 +1,119 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/app/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/app/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "image": null, + "execution_metadata": "PRIVATE DATA HIDDEN", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "fdf3851c-def8-4de1-87f1-6d4543189e22", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "docker", + "data": {} + }, + "execution_metadata": "[PRIVATE DATA HIDDEN IN LISTS]", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "image": "cloudfoundry/diego-docker-app-custom:latest", + "checksum": null, + "buildpacks": null, + "stack": null, + "created_at": "2016-03-17T00:00:01Z", + "updated_at": "2016-03-17T21:41:32Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/fdf3851c-def8-4de1-87f1-6d4543189e22" + }, + "package": { + "href": "https://api.example.org/v3/packages/c5725684-a02f-4e59-bc67-8f36ae944688" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/apps/GET_{id}_env_response.json b/tests/fixtures/v3/apps/GET_{id}_env_response.json new file mode 100644 index 0000000..7976bfe --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_env_response.json @@ -0,0 +1,50 @@ +{ + "staging_env_json": { + "GEM_CACHE": "http://gem-cache.example.org" + }, + "running_env_json": { + "HTTP_PROXY": "http://proxy.example.org" + }, + "environment_variables": { + "RAILS_ENV": "production" + }, + "system_env_json": { + "VCAP_SERVICES": { + "mysql": [ + { + "name": "db-for-my-app", + "label": "mysql", + "tags": [ + "relational", + "sql" + ], + "plan": "xlarge", + "credentials": { + "username": "user", + "password": "top-secret" + }, + "syslog_drain_url": "https://syslog.example.org/drain", + "provider": null + } + ] + } + }, + "application_env_json": { + "VCAP_APPLICATION": { + "limits": { + "fds": 16384 + }, + "application_name": "my_app", + "application_uris": [ + "my_app.example.org" + ], + "name": "my_app", + "space_name": "my_space", + "space_id": "2f35885d-0c9d-4423-83ad-fd05066f8576", + "uris": [ + "my_app.example.org" + ], + "users": null + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/apps/GET_{id}_environment_variables_response.json b/tests/fixtures/v3/apps/GET_{id}_environment_variables_response.json new file mode 100644 index 0000000..c0d016d --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_environment_variables_response.json @@ -0,0 +1,13 @@ +{ + "var": { + "RAILS_ENV": "production" + }, + "links": { + "self": { + "href": "https://api.example.org/v3/apps/[guid]/environment_variables" + }, + "app": { + "href": "https://api.example.org/v3/apps/[guid]" + } + } +} \ No newline at end of file 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/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/test/fixtures/v3/apps/GET_{id}_response.json b/tests/fixtures/v3/apps/GET_{id}_response.json similarity index 100% rename from test/fixtures/v3/apps/GET_{id}_response.json rename to tests/fixtures/v3/apps/GET_{id}_response.json diff --git a/tests/fixtures/v3/apps/GET_{id}_response_include_space_and_org.json b/tests/fixtures/v3/apps/GET_{id}_response_include_space_and_org.json new file mode 100644 index 0000000..5255f52 --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_response_include_space_and_org.json @@ -0,0 +1,107 @@ +{ + "guid": "app_id", + "name": "my_app", + "state": "STOPPED", + "created_at": "2016-03-17T21:41:30Z", + "updated_at": "2016-06-08T16:41:26Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": [ + "java_buildpack" + ], + "stack": "cflinuxfs2" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/apps/app_id" + }, + "space": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "http://somewhere.org/v3/apps/app_id/processes" + }, + "route_mappings": { + "href": "http://somewhere.org/v3/apps/app_id/route_mappings" + }, + "packages": { + "href": "http://somewhere.org/v3/apps/app_id/packages" + }, + "environment_variables": { + "href": "http://somewhere.org/v3/apps/app_id/environment_variables" + }, + "current_droplet": { + "href": "http://somewhere.org/v3/apps/app_id/droplets/current" + }, + "droplets": { + "href": "http://somewhere.org/v3/apps/app_id/droplets" + }, + "tasks": { + "href": "http://somewhere.org/v3/apps/app_id/tasks" + }, + "start": { + "href": "http://somewhere.org/v3/apps/app_id/actions/start", + "method": "POST" + }, + "stop": { + "href": "http://somewhere.org/v3/apps/app_id/actions/stop", + "method": "POST" + } + }, + "metadata": { + "labels": {} + }, + "included": { + "spaces": [ + { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my_space", + "relationships": { + "organization": { + "data": { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "organization": { + "href": "http://somewhere.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {} + } + } + ], + "organizations": [ + { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my_organization", + "links": { + "self": { + "href": "http://somewhere.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {} + } + } + ] + } +} \ 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/fixtures/v3/apps/GET_{id}_routes_response.json b/tests/fixtures/v3/apps/GET_{id}_routes_response.json new file mode 100644 index 0000000..9f8d95a --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_routes_response.json @@ -0,0 +1,88 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/routes?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/routes?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "http", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "http1", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "http1", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } + } + ] +} \ No newline at end of file diff --git a/test/fixtures/v3/apps/POST_{id}_actions_start_response.json b/tests/fixtures/v3/apps/POST_{id}_actions_start_response.json similarity index 96% rename from test/fixtures/v3/apps/POST_{id}_actions_start_response.json rename to tests/fixtures/v3/apps/POST_{id}_actions_start_response.json index 4d4880f..af1dfc7 100644 --- a/test/fixtures/v3/apps/POST_{id}_actions_start_response.json +++ b/tests/fixtures/v3/apps/POST_{id}_actions_start_response.json @@ -7,7 +7,9 @@ "lifecycle": { "type": "buildpack", "data": { - "buildpacks": ["java_buildpack"], + "buildpacks": [ + "java_buildpack" + ], "stack": "cflinuxfs2" } }, diff --git a/tests/fixtures/v3/audit_events/GET_response.json b/tests/fixtures/v3/audit_events/GET_response.json new file mode 100644 index 0000000..23a01aa --- /dev/null +++ b/tests/fixtures/v3/audit_events/GET_response.json @@ -0,0 +1,48 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/audit_events?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/audit_events?page=1&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "a595fe2f-01ff-4965-a50c-290258ab8582", + "created_at": "2016-06-08T16:41:23Z", + "updated_at": "2016-06-08T16:41:26Z", + "type": "audit.app.update", + "actor": { + "guid": "d144abe3-3d7b-40d4-b63f-2584798d3ee5", + "type": "user", + "name": "admin" + }, + "target": { + "guid": "2e3151ba-9a63-4345-9c5b-6d8c238f4e55", + "type": "app", + "name": "my-app" + }, + "data": { + "request": { + "recursive": true + } + }, + "space": { + "guid": "cb97dd25-d4f7-4185-9e6f-ad6e585c207c" + }, + "organization": { + "guid": "d9be96f5-ea8f-4549-923f-bec882e32e3c" + }, + "links": { + "self": { + "href": "https://api.example.org//v3/audit_events/a595fe2f-01ff-4965-a50c-290258ab8582" + } + } + } + ] +} diff --git a/tests/fixtures/v3/audit_events/GET_{id}_response.json b/tests/fixtures/v3/audit_events/GET_{id}_response.json new file mode 100644 index 0000000..58b9e1a --- /dev/null +++ b/tests/fixtures/v3/audit_events/GET_{id}_response.json @@ -0,0 +1,32 @@ +{ + "guid": "a595fe2f-01ff-4965-a50c-290258ab8582", + "created_at": "2016-06-08T16:41:23Z", + "updated_at": "2016-06-08T16:41:26Z", + "type": "audit.app.update", + "actor": { + "guid": "d144abe3-3d7b-40d4-b63f-2584798d3ee5", + "type": "user", + "name": "admin" + }, + "target": { + "guid": "2e3151ba-9a63-4345-9c5b-6d8c238f4e55", + "type": "app", + "name": "my-app" + }, + "data": { + "request": { + "recursive": true + } + }, + "space": { + "guid": "cb97dd25-d4f7-4185-9e6f-ad6e585c207c" + }, + "organization": { + "guid": "d9be96f5-ea8f-4549-923f-bec882e32e3c" + }, + "links": { + "self": { + "href": "https://api.example.org/v3/audit_events/a595fe2f-01ff-4965-a50c-290258ab8582" + } + } +} diff --git a/test/fixtures/v3/buildpacks/GET_response.json b/tests/fixtures/v3/buildpacks/GET_response.json similarity index 100% rename from test/fixtures/v3/buildpacks/GET_response.json rename to tests/fixtures/v3/buildpacks/GET_response.json diff --git a/test/fixtures/v3/buildpacks/GET_{id}_response.json b/tests/fixtures/v3/buildpacks/GET_{id}_response.json similarity index 73% rename from test/fixtures/v3/buildpacks/GET_{id}_response.json rename to tests/fixtures/v3/buildpacks/GET_{id}_response.json index dae324d..72d5b16 100644 --- a/test/fixtures/v3/buildpacks/GET_{id}_response.json +++ b/tests/fixtures/v3/buildpacks/GET_{id}_response.json @@ -10,16 +10,16 @@ "enabled": true, "locked": false, "metadata": { - "labels": { }, - "annotations": { } + "labels": {}, + "annotations": {} }, "links": { "self": { "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2" }, "upload": { - "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2/upload", - "method": "POST" + "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2/upload", + "method": "POST" } } } \ No newline at end of file diff --git a/test/fixtures/v3/buildpacks/PATCH_{id}_response.json b/tests/fixtures/v3/buildpacks/PATCH_{id}_response.json similarity index 73% rename from test/fixtures/v3/buildpacks/PATCH_{id}_response.json rename to tests/fixtures/v3/buildpacks/PATCH_{id}_response.json index 658a888..9b57f3c 100644 --- a/test/fixtures/v3/buildpacks/PATCH_{id}_response.json +++ b/tests/fixtures/v3/buildpacks/PATCH_{id}_response.json @@ -10,16 +10,16 @@ "enabled": true, "locked": false, "metadata": { - "labels": { }, - "annotations": { } + "labels": {}, + "annotations": {} }, "links": { "self": { "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2" }, "upload": { - "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2/upload", - "method": "POST" + "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2/upload", + "method": "POST" } } } diff --git a/test/fixtures/v3/buildpacks/POST_response.json b/tests/fixtures/v3/buildpacks/POST_response.json similarity index 73% rename from test/fixtures/v3/buildpacks/POST_response.json rename to tests/fixtures/v3/buildpacks/POST_response.json index dae324d..72d5b16 100644 --- a/test/fixtures/v3/buildpacks/POST_response.json +++ b/tests/fixtures/v3/buildpacks/POST_response.json @@ -10,16 +10,16 @@ "enabled": true, "locked": false, "metadata": { - "labels": { }, - "annotations": { } + "labels": {}, + "annotations": {} }, "links": { "self": { "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2" }, "upload": { - "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2/upload", - "method": "POST" + "href": "https://api.example.org/v3/buildpacks/fd35633f-5c5c-4e4e-a5a9-0722c970a9d2/upload", + "method": "POST" } } } \ No newline at end of file diff --git a/tests/fixtures/v3/domains/GET_response.json b/tests/fixtures/v3/domains/GET_response.json new file mode 100644 index 0000000..af86245 --- /dev/null +++ b/tests/fixtures/v3/domains/GET_response.json @@ -0,0 +1,43 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/domains?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/domains?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "name": "test-domain.com", + "internal": false, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "organization": { + "data": null + }, + "shared_organizations": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + }, + "route_reservations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/route_reservations" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/domains/GET_{id}_response.json b/tests/fixtures/v3/domains/GET_{id}_response.json new file mode 100644 index 0000000..91eb241 --- /dev/null +++ b/tests/fixtures/v3/domains/GET_{id}_response.json @@ -0,0 +1,42 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "name": "test-domain.com", + "internal": false, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "organization": { + "data": { + "guid": "3a3f3d89-3f89-4f05-8188-751b298c79d5" + } + }, + "shared_organizations": { + "data": [ + { + "guid": "404f3d89-3f89-6z72-8188-751b298d88d5" + }, + { + "guid": "416d3d89-3f89-8h67-2189-123b298d3592" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/3a3f3d89-3f89-4f05-8188-751b298c79d5" + }, + "route_reservations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/route_reservations" + }, + "shared_organizations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/relationships/shared_organizations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/domains/PATCH_{id}_response.json b/tests/fixtures/v3/domains/PATCH_{id}_response.json new file mode 100644 index 0000000..9942b22 --- /dev/null +++ b/tests/fixtures/v3/domains/PATCH_{id}_response.json @@ -0,0 +1,46 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "name": "test-domain.com", + "internal": false, + "metadata": { + "labels": { + "key": "value" + }, + "annotations": { + "note": "detailed information" + } + }, + "relationships": { + "organization": { + "data": { + "guid": "3a3f3d89-3f89-4f05-8188-751b298c79d5" + } + }, + "shared_organizations": { + "data": [ + { + "guid": "404f3d89-3f89-6z72-8188-751b298d88d5" + }, + { + "guid": "416d3d89-3f89-8h67-2189-123b298d3592" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/3a3f3d89-3f89-4f05-8188-751b298c79d5" + }, + "route_reservations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/route_reservations" + }, + "shared_organizations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/relationships/shared_organizations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/domains/POST_response.json b/tests/fixtures/v3/domains/POST_response.json new file mode 100644 index 0000000..91eb241 --- /dev/null +++ b/tests/fixtures/v3/domains/POST_response.json @@ -0,0 +1,42 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "name": "test-domain.com", + "internal": false, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "organization": { + "data": { + "guid": "3a3f3d89-3f89-4f05-8188-751b298c79d5" + } + }, + "shared_organizations": { + "data": [ + { + "guid": "404f3d89-3f89-6z72-8188-751b298d88d5" + }, + { + "guid": "416d3d89-3f89-8h67-2189-123b298d3592" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/3a3f3d89-3f89-4f05-8188-751b298c79d5" + }, + "route_reservations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/route_reservations" + }, + "shared_organizations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/relationships/shared_organizations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/domains/POST_{id}_relationships_shared_organizations_response.json b/tests/fixtures/v3/domains/POST_{id}_relationships_shared_organizations_response.json new file mode 100644 index 0000000..5c8631b --- /dev/null +++ b/tests/fixtures/v3/domains/POST_{id}_relationships_shared_organizations_response.json @@ -0,0 +1,10 @@ +{ + "data": [ + { + "guid": "404f3d89-3f89-6z72-8188-751b298d88d5" + }, + { + "guid": "416d3d89-3f89-8h67-2189-123b298d3592" + } + ] +} diff --git a/tests/fixtures/v3/droplets/GET_response.json b/tests/fixtures/v3/droplets/GET_response.json new file mode 100644 index 0000000..4d3121f --- /dev/null +++ b/tests/fixtures/v3/droplets/GET_response.json @@ -0,0 +1,119 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/droplets?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/droplets?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "image": null, + "execution_metadata": "PRIVATE DATA HIDDEN", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "fdf3851c-def8-4de1-87f1-6d4543189e22", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "docker", + "data": {} + }, + "execution_metadata": "[PRIVATE DATA HIDDEN IN LISTS]", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "image": "cloudfoundry/diego-docker-app-custom:latest", + "checksum": null, + "buildpacks": null, + "stack": null, + "created_at": "2016-03-17T00:00:01Z", + "updated_at": "2016-03-17T21:41:32Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/fdf3851c-def8-4de1-87f1-6d4543189e22" + }, + "package": { + "href": "https://api.example.org/v3/packages/c5725684-a02f-4e59-bc67-8f36ae944688" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} diff --git a/tests/fixtures/v3/droplets/GET_{id}_response.json b/tests/fixtures/v3/droplets/GET_{id}_response.json new file mode 100644 index 0000000..8c2ab9c --- /dev/null +++ b/tests/fixtures/v3/droplets/GET_{id}_response.json @@ -0,0 +1,59 @@ +{ + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "execution_metadata": "", + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "image": null, + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/droplets/PATCH_{id}_response.json b/tests/fixtures/v3/droplets/PATCH_{id}_response.json new file mode 100644 index 0000000..e3d6353 --- /dev/null +++ b/tests/fixtures/v3/droplets/PATCH_{id}_response.json @@ -0,0 +1,63 @@ +{ + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "execution_metadata": "", + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "image": null, + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": { + "release": "stable" + }, + "annotations": { + "note": "detailed information" + } + } + } \ No newline at end of file diff --git a/tests/fixtures/v3/droplets/POST_response.json b/tests/fixtures/v3/droplets/POST_response.json new file mode 100644 index 0000000..9707e9b --- /dev/null +++ b/tests/fixtures/v3/droplets/POST_response.json @@ -0,0 +1,57 @@ +{ + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "AWAITING_UPLOAD", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "execution_metadata": "", + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "image": null, + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "upload": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/upload", + "method": "POST" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/fake/GET_multi_page_0_response.json b/tests/fixtures/v3/fake/GET_multi_page_0_response.json new file mode 100644 index 0000000..df09c93 --- /dev/null +++ b/tests/fixtures/v3/fake/GET_multi_page_0_response.json @@ -0,0 +1,144 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "http://somewhere.org/fake" + }, + "last": { + "href": "http://somewhere.org/fake/last?page=2&per_page=2" + }, + "next": { + "href": "http://somewhere.org/fake/last?page=2&per_page=2" + }, + "previous": null + }, + "resources": [ + { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446", + "name": "my_app", + "state": "STARTED", + "created_at": "2016-03-17T21:41:30Z", + "updated_at": "2016-03-18T11:32:30Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": [ + "java_buildpack" + ], + "stack": "cflinuxfs2" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446" + }, + "space": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/processes" + }, + "route_mappings": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/route_mappings" + }, + "packages": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/packages" + }, + "environment_variables": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/environment_variables" + }, + "current_droplet": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets/current" + }, + "droplets": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets" + }, + "tasks": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/tasks" + }, + "start": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/start", + "method": "POST" + }, + "stop": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/stop", + "method": "POST" + } + }, + "metadata": { + "labels": {} + } + }, + { + "guid": "02b4ec9b-94c7-4468-9c23-4e906191a0f8", + "name": "my_app2", + "state": "STOPPED", + "created_at": "1970-01-01T00:00:02Z", + "updated_at": "2016-06-08T16:41:26Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": [ + "ruby_buildpack" + ], + "stack": "cflinuxfs2" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8" + }, + "space": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/processes" + }, + "route_mappings": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/route_mappings" + }, + "packages": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/packages" + }, + "environment_variables": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/environment_variables" + }, + "current_droplet": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets/current" + }, + "droplets": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets" + }, + "tasks": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/tasks" + }, + "start": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/start", + "method": "POST" + }, + "stop": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/stop", + "method": "POST" + } + }, + "metadata": { + "labels": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/fake/GET_multi_page_1_response.json b/tests/fixtures/v3/fake/GET_multi_page_1_response.json new file mode 100644 index 0000000..d28ef45 --- /dev/null +++ b/tests/fixtures/v3/fake/GET_multi_page_1_response.json @@ -0,0 +1,144 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "http://somewhere.org/fake" + }, + "last": { + "href": "http://somewhere.org/fake/last?page=2&per_page=2" + }, + "next": null, + "previous": { + "href": "http://somewhere.org/fake" + } + }, + "resources": [ + { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc447", + "name": "my_app3", + "state": "STARTED", + "created_at": "2016-03-17T21:41:30Z", + "updated_at": "2016-03-18T11:32:30Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": [ + "java_buildpack" + ], + "stack": "cflinuxfs2" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447" + }, + "space": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/processes" + }, + "route_mappings": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/route_mappings" + }, + "packages": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/packages" + }, + "environment_variables": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/environment_variables" + }, + "current_droplet": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/droplets/current" + }, + "droplets": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/droplets" + }, + "tasks": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/tasks" + }, + "start": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/actions/start", + "method": "POST" + }, + "stop": { + "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc447/actions/stop", + "method": "POST" + } + }, + "metadata": { + "labels": {} + } + }, + { + "guid": "02b4ec9b-94c7-4468-9c23-4e906191a0f9", + "name": "my_app4", + "state": "STOPPED", + "created_at": "1970-01-01T00:00:02Z", + "updated_at": "2016-06-08T16:41:26Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": [ + "ruby_buildpack" + ], + "stack": "cflinuxfs2" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9" + }, + "space": { + "href": "http://somewhere.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/processes" + }, + "route_mappings": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/route_mappings" + }, + "packages": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/packages" + }, + "environment_variables": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/environment_variables" + }, + "current_droplet": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/droplets/current" + }, + "droplets": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/droplets" + }, + "tasks": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/tasks" + }, + "start": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/actions/start", + "method": "POST" + }, + "stop": { + "href": "http://somewhere.org/v3/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f9/actions/stop", + "method": "POST" + } + }, + "metadata": { + "labels": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/feature_flags/GET_response.json b/tests/fixtures/v3/feature_flags/GET_response.json new file mode 100644 index 0000000..12cc15f --- /dev/null +++ b/tests/fixtures/v3/feature_flags/GET_response.json @@ -0,0 +1,38 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/feature_flags?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/feature_flags?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "name": "my_feature_flag", + "enabled": true, + "updated_at": "2016-10-17T20:00:42Z", + "custom_error_message": "error message the user sees", + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/my_feature_flag" + } + } + }, + { + "name": "my_second_feature_flag", + "enabled": false, + "updated_at": null, + "custom_error_message": null, + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/my_second_feature_flag" + } + } + } + ] +} diff --git a/tests/fixtures/v3/feature_flags/GET_{id}_response.json b/tests/fixtures/v3/feature_flags/GET_{id}_response.json new file mode 100644 index 0000000..c62bb80 --- /dev/null +++ b/tests/fixtures/v3/feature_flags/GET_{id}_response.json @@ -0,0 +1,11 @@ +{ + "name": "my_feature_flag", + "enabled": true, + "updated_at": "2016-10-17T20:00:42Z", + "custom_error_message": "error message the user sees", + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/my_feature_flag" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/feature_flags/PATCH_{id}_response.json b/tests/fixtures/v3/feature_flags/PATCH_{id}_response.json new file mode 100644 index 0000000..c62bb80 --- /dev/null +++ b/tests/fixtures/v3/feature_flags/PATCH_{id}_response.json @@ -0,0 +1,11 @@ +{ + "name": "my_feature_flag", + "enabled": true, + "updated_at": "2016-10-17T20:00:42Z", + "custom_error_message": "error message the user sees", + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/my_feature_flag" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/isolation_segments/GET_response.json b/tests/fixtures/v3/isolation_segments/GET_response.json new file mode 100644 index 0000000..9e768b1 --- /dev/null +++ b/tests/fixtures/v3/isolation_segments/GET_response.json @@ -0,0 +1,106 @@ +{ + "pagination": { + "total_results": 5, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/isolation_segments?page=1&per_page=5" + }, + "last": { + "href": "https://api.example.org/v3/isolation_segments?page=3&per_page=5" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c", + "name": "an_isolation_segment", + "created_at": "2016-10-19T20:25:04Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } + }, + { + "guid": "68d54d31-9b3a-463b-ba94-e8e4c32edbac", + "name": "an_isolation_segment1", + "created_at": "2016-10-19T20:29:19Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/68d54d31-9b3a-463b-ba94-e8e4c32edbac" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/68d54d31-9b3a-463b-ba94-e8e4c32edbac/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } + }, + { + "guid": "ecdc67c3-a71e-43ff-bddf-048930b8cd03", + "name": "an_isolation_segment2", + "created_at": "2016-10-19T20:29:22Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/ecdc67c3-a71e-43ff-bddf-048930b8cd03" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/ecdc67c3-a71e-43ff-bddf-048930b8cd03/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } + }, + { + "guid": "424c89e4-4353-46b7-9bf4-f90bd9bacac0", + "name": "an_isolation_segment3", + "created_at": "2016-10-19T20:29:27Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/424c89e4-4353-46b7-9bf4-f90bd9bacac0" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/424c89e4-4353-46b7-9bf4-f90bd9bacac0/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } + }, + { + "guid": "0a79fcec-a648-4eb8-a6c3-2b5be39047c7", + "name": "an_isolation_segment4", + "created_at": "2016-10-19T20:29:33Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/0a79fcec-a648-4eb8-a6c3-2b5be39047c7" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/0a79fcec-a648-4eb8-a6c3-2b5be39047c7/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } + } + ] +} diff --git a/tests/fixtures/v3/isolation_segments/GET_{id}_relationships_organizations_response.json b/tests/fixtures/v3/isolation_segments/GET_{id}_relationships_organizations_response.json new file mode 100644 index 0000000..c9674d8 --- /dev/null +++ b/tests/fixtures/v3/isolation_segments/GET_{id}_relationships_organizations_response.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "guid": "68d54d31-9b3a-463b-ba94-e8e4c32edbac" + }, + { + "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/relationships/organizations" + }, + "related": { + "href": "https://api.example.org/v3/isolation_segments/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/organizations" + } + } +} diff --git a/tests/fixtures/v3/isolation_segments/GET_{id}_relationships_spaces_response.json b/tests/fixtures/v3/isolation_segments/GET_{id}_relationships_spaces_response.json new file mode 100644 index 0000000..f6bb561 --- /dev/null +++ b/tests/fixtures/v3/isolation_segments/GET_{id}_relationships_spaces_response.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920" + }, + { + "guid": "d4c91047-7b29-4fda-b7f9-04033e5c9c9f" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/relationships/spaces" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/isolation_segments/GET_{id}_response.json b/tests/fixtures/v3/isolation_segments/GET_{id}_response.json new file mode 100644 index 0000000..2aa2be3 --- /dev/null +++ b/tests/fixtures/v3/isolation_segments/GET_{id}_response.json @@ -0,0 +1,19 @@ +{ + "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c", + "name": "an_isolation_segment", + "created_at": "2016-10-19T20:25:04Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } +} + diff --git a/tests/fixtures/v3/isolation_segments/PATCH_{id}_response.json b/tests/fixtures/v3/isolation_segments/PATCH_{id}_response.json new file mode 100644 index 0000000..1a1a930 --- /dev/null +++ b/tests/fixtures/v3/isolation_segments/PATCH_{id}_response.json @@ -0,0 +1,18 @@ +{ + "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c", + "name": "my_isolation_segment", + "created_at": "2016-10-19T20:25:04Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } +} diff --git a/tests/fixtures/v3/isolation_segments/POST_response.json b/tests/fixtures/v3/isolation_segments/POST_response.json new file mode 100644 index 0000000..2bc5b59 --- /dev/null +++ b/tests/fixtures/v3/isolation_segments/POST_response.json @@ -0,0 +1,18 @@ +{ + "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c", + "name": "an_isolation_segment", + "created_at": "2016-10-19T20:25:04Z", + "updated_at": "2016-11-08T16:41:26Z", + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c" + }, + "organizations": { + "href": "https://api.example.org/v3/isolation_segments/b19f6525-cbd3-4155-b156-dc0c2a431b4c/organizations" + } + }, + "metadata": { + "annotations": {}, + "labels": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/isolation_segments/POST_{id}_relationships_organizations_response.json b/tests/fixtures/v3/isolation_segments/POST_{id}_relationships_organizations_response.json new file mode 100644 index 0000000..4bc1c91 --- /dev/null +++ b/tests/fixtures/v3/isolation_segments/POST_{id}_relationships_organizations_response.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "guid": "68d54d31-9b3a-463b-ba94-e8e4c32edbac" + }, + { + "guid": "b19f6525-cbd3-4155-b156-dc0c2a431b4c" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/isolation_segments/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/relationships/organizations" + }, + "related": { + "href": "https://api.example.org/v3/isolation_segments/bdeg4371-cbd3-4155-b156-dc0c2a431b4c/organizations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/jobs/GET_{id}_complete_response.json b/tests/fixtures/v3/jobs/GET_{id}_complete_response.json new file mode 100644 index 0000000..54aa460 --- /dev/null +++ b/tests/fixtures/v3/jobs/GET_{id}_complete_response.json @@ -0,0 +1,14 @@ +{ + "guid": "b19ae525-cbd3-4155-b156-dc0c2a431b4c", + "created_at": "2016-10-19T20:25:04Z", + "updated_at": "2016-11-08T16:41:26Z", + "operation": "app.delete", + "state": "COMPLETE", + "links": { + "self": { + "href": "https://api.example.org/v3/jobs/b19ae525-cbd3-4155-b156-dc0c2a431b4c" + } + }, + "errors": [], + "warnings": [] + } \ No newline at end of file diff --git a/tests/fixtures/v3/jobs/GET_{id}_failed_response.json b/tests/fixtures/v3/jobs/GET_{id}_failed_response.json new file mode 100644 index 0000000..867a6a9 --- /dev/null +++ b/tests/fixtures/v3/jobs/GET_{id}_failed_response.json @@ -0,0 +1,27 @@ +{ + "guid": "b19ae525-cbd3-4155-b156-dc0c2a431b4c", + "created_at": "2016-10-19T20:25:04Z", + "updated_at": "2016-11-08T16:41:26Z", + "operation": "app.delete", + "state": "FAILED", + "links": { + "self": { + "href": "https://api.example.org/v3/jobs/b19ae525-cbd3-4155-b156-dc0c2a431b4c" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + }, + "errors": [ + { + "code": 10008, + "title": "CF-UnprocessableEntity", + "detail": "something went wrong" + } + ], + "warnings": [ + { + "detail": "warning! warning!" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/jobs/GET_{id}_processing_response.json b/tests/fixtures/v3/jobs/GET_{id}_processing_response.json new file mode 100644 index 0000000..3fac0ea --- /dev/null +++ b/tests/fixtures/v3/jobs/GET_{id}_processing_response.json @@ -0,0 +1,14 @@ +{ + "guid": "b19ae525-cbd3-4155-b156-dc0c2a431b4c", + "created_at": "2016-10-19T20:25:04Z", + "updated_at": "2016-11-08T16:41:26Z", + "operation": "app.delete", + "state": "PROCESSING", + "links": { + "self": { + "href": "https://api.example.org/v3/jobs/b19ae525-cbd3-4155-b156-dc0c2a431b4c" + } + }, + "errors": [], + "warnings": [] + } \ No newline at end of file diff --git a/tests/fixtures/v3/organization_quotas/GET_response.json b/tests/fixtures/v3/organization_quotas/GET_response.json new file mode 100644 index 0000000..d57f146 --- /dev/null +++ b/tests/fixtures/v3/organization_quotas/GET_response.json @@ -0,0 +1,88 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/organization_quotas?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/organization_quotas?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "quota-1-guid", + "created_at": "2016-05-04T17:00:41Z", + "updated_at": "2016-05-04T17:00:41Z", + "name": "don-quixote", + "apps": { + "total_memory_in_mb": 5120, + "per_process_memory_in_mb": 1024, + "total_instances": 10, + "per_app_tasks": 5 + }, + "services": { + "paid_services_allowed": true, + "total_service_instances": 10, + "total_service_keys": 20 + }, + "routes": { + "total_routes": 8, + "total_reserved_ports": 4 + }, + "domains": { + "total_domains": 7 + }, + "relationships": { + "organizations": { + "data": [ + { + "guid": "9b370018-c38e-44c9-86d6-155c76801104" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organization_quotas/quota-1-guid" + } + } + }, + { + "guid": "quota-2-guid", + "created_at": "2017-05-04T17:00:41Z", + "updated_at": "2017-05-04T17:00:41Z", + "name": "sancho-panza", + "apps": { + "total_memory_in_mb": 2048, + "per_process_memory_in_mb": 1024, + "total_instances": 5, + "per_app_tasks": 2 + }, + "services": { + "paid_services_allowed": true, + "total_service_instances": 10, + "total_service_keys": 20 + }, + "routes": { + "total_routes": 8, + "total_reserved_ports": 4 + }, + "domains": { + "total_domains": 7 + }, + "relationships": { + "organizations": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organization_quotas/quota-2-guid" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/organization_quotas/GET_{id}_response.json b/tests/fixtures/v3/organization_quotas/GET_{id}_response.json new file mode 100644 index 0000000..0840545 --- /dev/null +++ b/tests/fixtures/v3/organization_quotas/GET_{id}_response.json @@ -0,0 +1,38 @@ +{ + "guid": "quota-guid", + "created_at": "2016-05-04T17:00:41Z", + "updated_at": "2016-05-04T17:00:41Z", + "name": "don-quixote", + "apps": { + "total_memory_in_mb": 5120, + "per_process_memory_in_mb": 1024, + "total_instances": 10, + "per_app_tasks": 5 + }, + "services": { + "paid_services_allowed": true, + "total_service_instances": 10, + "total_service_keys": 20 + }, + "routes": { + "total_routes": 8, + "total_reserved_ports": 4 + }, + "domains": { + "total_domains": 7 + }, + "relationships": { + "organizations": { + "data": [ + { + "guid": "9b370018-c38e-44c9-86d6-155c76801104" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organization_quotas/quota-guid" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/organization_quotas/PATCH_{id}_response.json b/tests/fixtures/v3/organization_quotas/PATCH_{id}_response.json new file mode 100644 index 0000000..0840545 --- /dev/null +++ b/tests/fixtures/v3/organization_quotas/PATCH_{id}_response.json @@ -0,0 +1,38 @@ +{ + "guid": "quota-guid", + "created_at": "2016-05-04T17:00:41Z", + "updated_at": "2016-05-04T17:00:41Z", + "name": "don-quixote", + "apps": { + "total_memory_in_mb": 5120, + "per_process_memory_in_mb": 1024, + "total_instances": 10, + "per_app_tasks": 5 + }, + "services": { + "paid_services_allowed": true, + "total_service_instances": 10, + "total_service_keys": 20 + }, + "routes": { + "total_routes": 8, + "total_reserved_ports": 4 + }, + "domains": { + "total_domains": 7 + }, + "relationships": { + "organizations": { + "data": [ + { + "guid": "9b370018-c38e-44c9-86d6-155c76801104" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organization_quotas/quota-guid" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/organization_quotas/POST_response.json b/tests/fixtures/v3/organization_quotas/POST_response.json new file mode 100644 index 0000000..0840545 --- /dev/null +++ b/tests/fixtures/v3/organization_quotas/POST_response.json @@ -0,0 +1,38 @@ +{ + "guid": "quota-guid", + "created_at": "2016-05-04T17:00:41Z", + "updated_at": "2016-05-04T17:00:41Z", + "name": "don-quixote", + "apps": { + "total_memory_in_mb": 5120, + "per_process_memory_in_mb": 1024, + "total_instances": 10, + "per_app_tasks": 5 + }, + "services": { + "paid_services_allowed": true, + "total_service_instances": 10, + "total_service_keys": 20 + }, + "routes": { + "total_routes": 8, + "total_reserved_ports": 4 + }, + "domains": { + "total_domains": 7 + }, + "relationships": { + "organizations": { + "data": [ + { + "guid": "9b370018-c38e-44c9-86d6-155c76801104" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organization_quotas/quota-guid" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/organization_quotas/POST_{id}_organizations_response.json b/tests/fixtures/v3/organization_quotas/POST_{id}_organizations_response.json new file mode 100644 index 0000000..a937d7c --- /dev/null +++ b/tests/fixtures/v3/organization_quotas/POST_{id}_organizations_response.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "guid": "org-guid1" + }, + { + "guid": "org-guid2" + }, + { + "guid": "previous-org-guid" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/organization_quotas/quota-guid/relationships/organizations" + } + } +} \ No newline at end of file diff --git a/test/fixtures/v3/organizations/GET_response.json b/tests/fixtures/v3/organizations/GET_response.json similarity index 100% rename from test/fixtures/v3/organizations/GET_response.json rename to tests/fixtures/v3/organizations/GET_response.json diff --git a/tests/fixtures/v3/organizations/GET_{id}_default_domain_response.json b/tests/fixtures/v3/organizations/GET_{id}_default_domain_response.json new file mode 100644 index 0000000..91eb241 --- /dev/null +++ b/tests/fixtures/v3/organizations/GET_{id}_default_domain_response.json @@ -0,0 +1,42 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "name": "test-domain.com", + "internal": false, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "organization": { + "data": { + "guid": "3a3f3d89-3f89-4f05-8188-751b298c79d5" + } + }, + "shared_organizations": { + "data": [ + { + "guid": "404f3d89-3f89-6z72-8188-751b298d88d5" + }, + { + "guid": "416d3d89-3f89-8h67-2189-123b298d3592" + } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/3a3f3d89-3f89-4f05-8188-751b298c79d5" + }, + "route_reservations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/route_reservations" + }, + "shared_organizations": { + "href": "https://api.example.org/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5/relationships/shared_organizations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/organizations/GET_{id}_domains_response.json b/tests/fixtures/v3/organizations/GET_{id}_domains_response.json new file mode 100644 index 0000000..92e6fd4 --- /dev/null +++ b/tests/fixtures/v3/organizations/GET_{id}_domains_response.json @@ -0,0 +1,48 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/organizations/016b770b-b447-4a12-800a-1b4c69406a9f/domains?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/organizations/016b770b-b447-4a12-800a-1b4c69406a9f/domains?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "name": "test-domain.com", + "internal": false, + "router_group": { "guid": "5806148f-cce6-4d86-7fbd-aa269e3f6f3f" }, + "supported_protocols": ["tcp"], + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "organization": { + "data": null + }, + "shared_organizations": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/domains/016b770b-b447-4a12-800a-1b4c69406a9f" + }, + "route_reservations": { + "href": "https://api.example.org/v3/domains/016b770b-b447-4a12-800a-1b4c69406a9f/route_reservations" + }, + "router_group": { + "href": "https://api.example.org/routing/v1/router_groups/5806148f-cce6-4d86-7fbd-aa269e3f6f3f" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/organizations/GET_{id}_relationships_default_isolation_segment_response.json b/tests/fixtures/v3/organizations/GET_{id}_relationships_default_isolation_segment_response.json new file mode 100644 index 0000000..c585f23 --- /dev/null +++ b/tests/fixtures/v3/organizations/GET_{id}_relationships_default_isolation_segment_response.json @@ -0,0 +1,13 @@ +{ + "data": { + "guid": "9d8e007c-ce52-4ea7-8a57-f2825d2c6b39" + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organizations/d4c91047-7b29-4fda-b7f9-04033e5c9c9f/relationships/default_isolation_segment" + }, + "related": { + "href": "https://api.example.org/v3/isolation_segments/9d8e007c-ce52-4ea7-8a57-f2825d2c6b39" + } + } +} \ No newline at end of file diff --git a/test/fixtures/v3/organizations/GET_{id}_response.json b/tests/fixtures/v3/organizations/GET_{id}_response.json similarity index 100% rename from test/fixtures/v3/organizations/GET_{id}_response.json rename to tests/fixtures/v3/organizations/GET_{id}_response.json diff --git a/tests/fixtures/v3/organizations/GET_{id}_usage_summary_response.json b/tests/fixtures/v3/organizations/GET_{id}_usage_summary_response.json new file mode 100644 index 0000000..b7a31e7 --- /dev/null +++ b/tests/fixtures/v3/organizations/GET_{id}_usage_summary_response.json @@ -0,0 +1,14 @@ +{ + "usage_summary": { + "started_instances": 3, + "memory_in_mb": 50 + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organizations/d4c91047-7b29-4fda-b7f9-04033e5c9c9f/usage_summary" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/d4c91047-7b29-4fda-b7f9-04033e5c9c9f" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/organizations/PATCH_{id}_relationships_default_isolation_segment_response.json b/tests/fixtures/v3/organizations/PATCH_{id}_relationships_default_isolation_segment_response.json new file mode 100644 index 0000000..c585f23 --- /dev/null +++ b/tests/fixtures/v3/organizations/PATCH_{id}_relationships_default_isolation_segment_response.json @@ -0,0 +1,13 @@ +{ + "data": { + "guid": "9d8e007c-ce52-4ea7-8a57-f2825d2c6b39" + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organizations/d4c91047-7b29-4fda-b7f9-04033e5c9c9f/relationships/default_isolation_segment" + }, + "related": { + "href": "https://api.example.org/v3/isolation_segments/9d8e007c-ce52-4ea7-8a57-f2825d2c6b39" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/organizations/PATCH_{id}_response.json b/tests/fixtures/v3/organizations/PATCH_{id}_response.json new file mode 100644 index 0000000..33f7326 --- /dev/null +++ b/tests/fixtures/v3/organizations/PATCH_{id}_response.json @@ -0,0 +1,29 @@ +{ + "guid": "24637893-3b77-489d-bb79-8466f0d88b52", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my-organization", + "suspended": false, + "relationships": { + "quota": { + "data": { + "guid": "b7887f5c-34bb-40c5-9778-577572e4fb2d" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organizations/24637893-3b77-489d-bb79-8466f0d88b52" + }, + "domains": { + "href": "https://api.example.org/v3/organizations/24637893-3b77-489d-bb79-8466f0d88b52/domains" + }, + "default_domain": { + "href": "https://api.example.org/v3/organizations/24637893-3b77-489d-bb79-8466f0d88b52/domains/default" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/organizations/POST_response.json b/tests/fixtures/v3/organizations/POST_response.json new file mode 100644 index 0000000..33f7326 --- /dev/null +++ b/tests/fixtures/v3/organizations/POST_response.json @@ -0,0 +1,29 @@ +{ + "guid": "24637893-3b77-489d-bb79-8466f0d88b52", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my-organization", + "suspended": false, + "relationships": { + "quota": { + "data": { + "guid": "b7887f5c-34bb-40c5-9778-577572e4fb2d" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/organizations/24637893-3b77-489d-bb79-8466f0d88b52" + }, + "domains": { + "href": "https://api.example.org/v3/organizations/24637893-3b77-489d-bb79-8466f0d88b52/domains" + }, + "default_domain": { + "href": "https://api.example.org/v3/organizations/24637893-3b77-489d-bb79-8466f0d88b52/domains/default" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/packages/GET_response.json b/tests/fixtures/v3/packages/GET_response.json new file mode 100644 index 0000000..89964d7 --- /dev/null +++ b/tests/fixtures/v3/packages/GET_response.json @@ -0,0 +1,88 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/packages?types=bits%2Cdocker&page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/packages?types=bits%2Cdocker&page=1&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "a57fd932-85db-483a-a27e-b00efbb3b0a4", + "type": "bits", + "data": { + "checksum": { + "type": "sha256", + "value": null + }, + "error": null + }, + "state": "AWAITING_UPLOAD", + "created_at": "2015-11-03T00:53:54Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "app": { + "data": { + "guid": "fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/packages/a57fd932-85db-483a-a27e-b00efbb3b0a4" + }, + "upload": { + "href": "https://api.example.org/v3/packages/a57fd932-85db-483a-a27e-b00efbb3b0a4/upload", + "method": "POST" + }, + "download": { + "href": "https://api.example.org/v3/packages/a57fd932-85db-483a-a27e-b00efbb3b0a4/download", + "method": "GET" + }, + "app": { + "href": "https://api.example.org/v3/apps/fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "8f1f294d-cef8-4c11-9f0b-3bcdc0bd2691", + "type": "docker", + "data": { + "image": "registry/image:latest", + "username": "username", + "password": "***" + }, + "state": "READY", + "created_at": "2015-11-03T00:53:54Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "app": { + "data": { + "guid": "fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/packages/8f1f294d-cef8-4c11-9f0b-3bcdc0bd2691" + }, + "app": { + "href": "https://api.example.org/v3/apps/fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/packages/GET_{id}_droplets_response.json b/tests/fixtures/v3/packages/GET_{id}_droplets_response.json new file mode 100644 index 0000000..b8d2c83 --- /dev/null +++ b/tests/fixtures/v3/packages/GET_{id}_droplets_response.json @@ -0,0 +1,119 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/packages/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/packages/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "image": null, + "execution_metadata": "PRIVATE DATA HIDDEN", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "fdf3851c-def8-4de1-87f1-6d4543189e22", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "docker", + "data": {} + }, + "execution_metadata": "[PRIVATE DATA HIDDEN IN LISTS]", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "image": "cloudfoundry/diego-docker-app-custom:latest", + "checksum": null, + "buildpacks": null, + "stack": null, + "created_at": "2016-03-17T00:00:01Z", + "updated_at": "2016-03-17T21:41:32Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/fdf3851c-def8-4de1-87f1-6d4543189e22" + }, + "package": { + "href": "https://api.example.org/v3/packages/c5725684-a02f-4e59-bc67-8f36ae944688" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/packages/GET_{id}_response.json b/tests/fixtures/v3/packages/GET_{id}_response.json new file mode 100644 index 0000000..3e72e01 --- /dev/null +++ b/tests/fixtures/v3/packages/GET_{id}_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/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/fixtures/v3/processes/GET_response.json b/tests/fixtures/v3/processes/GET_response.json new file mode 100644 index 0000000..34212a8 --- /dev/null +++ b/tests/fixtures/v3/processes/GET_response.json @@ -0,0 +1,112 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/processes?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/processes?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "6a901b7c-9417-4dc1-8189-d3234aa0ab82", + "type": "web", + "command": "[PRIVATE DATA HIDDEN IN LISTS]", + "instances": 5, + "memory_in_mb": 256, + "disk_in_mb": 1024, + "health_check": { + "type": "port", + "data": { + "timeout": null + } + }, + "relationships": { + "app": { + "data": { + "guid": "ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" + } + }, + "revision": { + "data": { + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "created_at": "2016-03-23T18:48:22Z", + "updated_at": "2016-03-23T18:48:42Z", + "links": { + "self": { + "href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82" + }, + "scale": { + "href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/actions/scale", + "method": "POST" + }, + "app": { + "href": "https://api.example.org/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "stats": { + "href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/stats" + } + } + }, + { + "guid": "3fccacd9-4b02-4b96-8d02-8e865865e9eb", + "type": "worker", + "command": "[PRIVATE DATA HIDDEN IN LISTS]", + "instances": 1, + "memory_in_mb": 256, + "disk_in_mb": 1024, + "health_check": { + "type": "process", + "data": { + "timeout": null + } + }, + "relationships": { + "app": { + "data": { + "guid": "ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" + } + }, + "revision": null + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "created_at": "2016-03-23T18:48:22Z", + "updated_at": "2016-03-23T18:48:42Z", + "links": { + "self": { + "href": "https://api.example.org/v3/processes/3fccacd9-4b02-4b96-8d02-8e865865e9eb" + }, + "scale": { + "href": "https://api.example.org/v3/processes/3fccacd9-4b02-4b96-8d02-8e865865e9eb/actions/scale", + "method": "POST" + }, + "app": { + "href": "https://api.example.org/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "stats": { + "href": "https://api.example.org/v3/processes/3fccacd9-4b02-4b96-8d02-8e865865e9eb/stats" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/processes/GET_{id}_response.json b/tests/fixtures/v3/processes/GET_{id}_response.json new file mode 100644 index 0000000..c416dcb --- /dev/null +++ b/tests/fixtures/v3/processes/GET_{id}_response.json @@ -0,0 +1,50 @@ +{ + "guid": "process_id", + "type": "web", + "command": "rackup", + "instances": 5, + "memory_in_mb": 256, + "disk_in_mb": 1024, + "health_check": { + "type": "port", + "data": { + "timeout": null + } + }, + "relationships": { + "app": { + "data": { + "guid": "ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" + } + }, + "revision": { + "data": { + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920" + } + } + }, + "metadata": { + "labels": { }, + "annotations": { } + }, + "created_at": "2016-03-23T18:48:22Z", + "updated_at": "2016-03-23T18:48:42Z", + "links": { + "self": { + "href": "https://api.example.org/v3/processes/process_id" + }, + "scale": { + "href": "https://api.example.org/v3/processes/process_id/actions/scale", + "method": "POST" + }, + "app": { + "href": "https://api.example.org/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "stats": { + "href": "https://api.example.org/v3/processes/process_id/stats" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/processes/GET_{id}_stats_response.json b/tests/fixtures/v3/processes/GET_{id}_stats_response.json new file mode 100644 index 0000000..74e545b --- /dev/null +++ b/tests/fixtures/v3/processes/GET_{id}_stats_response.json @@ -0,0 +1,30 @@ +{ + "resources": [ + { + "type": "web", + "index": 0, + "state": "RUNNING", + "usage": { + "time": "2016-03-23T23:17:30.476314154Z", + "cpu": 0.00038711029163348665, + "mem": 19177472, + "disk": 69705728 + }, + "host": "10.244.16.10", + "instance_ports": [ + { + "external": 64546, + "internal": 8080, + "external_tls_proxy_port": 61002, + "internal_tls_proxy_port": 61003 + } + ], + "uptime": 9042, + "mem_quota": 268435456, + "disk_quota": 1073741824, + "fds_quota": 16384, + "isolation_segment": "example_iso_segment", + "details": null + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/roles/GET_response.json b/tests/fixtures/v3/roles/GET_response.json new file mode 100644 index 0000000..af32af0 --- /dev/null +++ b/tests/fixtures/v3/roles/GET_response.json @@ -0,0 +1,82 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/roles?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/roles?page=2&per_page=2" + }, + "next": { + "href": null + }, + "previous": null + }, + "resources": [ + { + "guid": "40557c70-d1bd-4976-a2ab-a85f5e882418", + "created_at": "2019-10-10T17:19:12Z", + "updated_at": "2019-10-10T17:19:12Z", + "type": "organization_auditor", + "relationships": { + "user": { + "data": { + "guid": "59eadb5f-fc13-414f-84ba-77a35e239cc8" + } + }, + "organization": { + "data": { + "guid": "05c5da3b-6cbc-421c-87c3-20bb3c41ab7c" + } + }, + "space": { + "data": null + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/roles/40557c70-d1bd-4976-a2ab-a85f5e882418" + }, + "user": { + "href": "https://api.example.org/v3/users/59eadb5f-fc13-414f-84ba-77a35e239cc8" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/05c5da3b-6cbc-421c-87c3-20bb3c41ab7c" + } + } + }, + { + "guid": "12347c70-d1bd-4976-a2ab-a85f5e882418", + "created_at": "2047-11-10T17:19:12Z", + "updated_at": "2047-11-10T17:19:12Z", + "type": "space_auditor", + "relationships": { + "user": { + "data": { + "guid": "47eadb5f-fc13-414f-84ba-47a35e239cc8" + } + }, + "organization": { + "data": null + }, + "space": { + "data": { + "guid": "47c5da3b-6cbc-421c-87c3-20bb3c41ab7c" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/roles/12347c70-d1bd-4976-a2ab-a85f5e882418" + }, + "user": { + "href": "https://api.example.org/v3/users/47eadb5f-fc13-414f-84ba-77a35e239cc8" + }, + "space": { + "href": "https://api.example.org/v3/spaces/47c5da3b-6cbc-421c-87c3-20bb3c41ab7c" + } + } + } + ] + } \ No newline at end of file diff --git a/tests/fixtures/v3/roles/GET_{id}_response.json b/tests/fixtures/v3/roles/GET_{id}_response.json new file mode 100644 index 0000000..f8e9ede --- /dev/null +++ b/tests/fixtures/v3/roles/GET_{id}_response.json @@ -0,0 +1,32 @@ +{ + "guid": "role_id", + "created_at": "2019-10-10T17:19:12Z", + "updated_at": "2019-10-10T17:19:12Z", + "type": "organization_auditor", + "relationships": { + "user": { + "data": { + "guid": "59eadb5f-fc13-414f-84ba-77a35e239cc8" + } + }, + "organization": { + "data": { + "guid": "05c5da3b-6cbc-421c-87c3-20bb3c41ab7c" + } + }, + "space": { + "data": null + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/roles/40557c70-d1bd-4976-a2ab-a85f5e882418" + }, + "user": { + "href": "https://api.example.org/v3/users/59eadb5f-fc13-414f-84ba-77a35e239cc8" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/05c5da3b-6cbc-421c-87c3-20bb3c41ab7c" + } + } + } \ No newline at end of file diff --git a/tests/fixtures/v3/routes/GET_response.json b/tests/fixtures/v3/routes/GET_response.json new file mode 100644 index 0000000..680e80e --- /dev/null +++ b/tests/fixtures/v3/routes/GET_response.json @@ -0,0 +1,88 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/routes?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/routes?page=1&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "http", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "http1", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "http1", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } + } + ] +} diff --git a/tests/fixtures/v3/routes/GET_{id}_response.json b/tests/fixtures/v3/routes/GET_{id}_response.json new file mode 100644 index 0000000..8284103 --- /dev/null +++ b/tests/fixtures/v3/routes/GET_{id}_response.json @@ -0,0 +1,73 @@ +{ + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "tcp", + "port": 6666, + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/routes/PATCH_{id}_response.json b/tests/fixtures/v3/routes/PATCH_{id}_response.json new file mode 100644 index 0000000..82d9734 --- /dev/null +++ b/tests/fixtures/v3/routes/PATCH_{id}_response.json @@ -0,0 +1,77 @@ +{ + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "tcp", + "port": 6666, + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": { + "key": "value" + }, + "annotations": { + "note": "detailed information" + } + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/routes/POST_response.json b/tests/fixtures/v3/routes/POST_response.json new file mode 100644 index 0000000..ec6f099 --- /dev/null +++ b/tests/fixtures/v3/routes/POST_response.json @@ -0,0 +1,73 @@ +{ + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "tcp", + "port": 6666, + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {"key":"value"}, + "annotations": {"note":"detailed information"} + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/security_groups/GET_response.json b/tests/fixtures/v3/security_groups/GET_response.json new file mode 100644 index 0000000..4318db5 --- /dev/null +++ b/tests/fixtures/v3/security_groups/GET_response.json @@ -0,0 +1,84 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/security_groups?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/security_groups?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "b85a788e-671f-4549-814d-e34cdb2f539a", + "created_at": "2020-02-20T17:42:08Z", + "updated_at": "2020-02-20T17:42:08Z", + "name": "my-group0", + "globally_enabled": { + "running": true, + "staging": false + }, + "rules": [ + { + "protocol": "tcp", + "destination": "10.10.10.0/24", + "ports": "443,80,8080" + }, + { + "protocol": "icmp", + "destination": "10.10.10.0/24", + "type": 8, + "code": 0, + "description": "Allow ping requests to private services" + } + ], + "relationships": { + "staging_spaces": { + "data": [ + { + "guid": "space-guid-1" + }, + { + "guid": "space-guid-2" + } + ] + }, + "running_spaces": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/b85a788e-671f-4549-814d-e34cdb2f539a" + } + } + }, + { + "guid": "a89a788e-671f-4549-814d-e34c1b2f533a", + "created_at": "2020-02-20T17:42:08Z", + "updated_at": "2020-02-20T17:42:08Z", + "name": "my-group1", + "globally_enabled": { + "running": true, + "staging": true + }, + "rules": [], + "relationships": { + "staging_spaces": { + "data": [] + }, + "running_spaces": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/a89a788e-671f-4549-814d-e34c1b2f533a" + } + } + } + ] +} diff --git a/tests/fixtures/v3/security_groups/GET_{id}_response.json b/tests/fixtures/v3/security_groups/GET_{id}_response.json new file mode 100644 index 0000000..65c6e2a --- /dev/null +++ b/tests/fixtures/v3/security_groups/GET_{id}_response.json @@ -0,0 +1,44 @@ +{ + "guid": "b85a788e-671f-4549-814d-e34cdb2f539a", + "created_at": "2020-02-20T17:42:08Z", + "updated_at": "2020-02-20T17:42:08Z", + "name": "my-group0", + "globally_enabled": { + "running": true, + "staging": false + }, + "rules": [ + { + "protocol": "tcp", + "destination": "10.10.10.0/24", + "ports": "443,80,8080" + }, + { + "protocol": "icmp", + "destination": "10.10.10.0/24", + "type": 8, + "code": 0, + "description": "Allow ping requests to private services" + } + ], + "relationships": { + "staging_spaces": { + "data": [ + { + "guid": "space-guid-1" + }, + { + "guid": "space-guid-2" + } + ] + }, + "running_spaces": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/b85a788e-671f-4549-814d-e34cdb2f539a" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/security_groups/PATCH_{id}_response.json b/tests/fixtures/v3/security_groups/PATCH_{id}_response.json new file mode 100644 index 0000000..65c6e2a --- /dev/null +++ b/tests/fixtures/v3/security_groups/PATCH_{id}_response.json @@ -0,0 +1,44 @@ +{ + "guid": "b85a788e-671f-4549-814d-e34cdb2f539a", + "created_at": "2020-02-20T17:42:08Z", + "updated_at": "2020-02-20T17:42:08Z", + "name": "my-group0", + "globally_enabled": { + "running": true, + "staging": false + }, + "rules": [ + { + "protocol": "tcp", + "destination": "10.10.10.0/24", + "ports": "443,80,8080" + }, + { + "protocol": "icmp", + "destination": "10.10.10.0/24", + "type": 8, + "code": 0, + "description": "Allow ping requests to private services" + } + ], + "relationships": { + "staging_spaces": { + "data": [ + { + "guid": "space-guid-1" + }, + { + "guid": "space-guid-2" + } + ] + }, + "running_spaces": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/b85a788e-671f-4549-814d-e34cdb2f539a" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/security_groups/POST_response.json b/tests/fixtures/v3/security_groups/POST_response.json new file mode 100644 index 0000000..65c6e2a --- /dev/null +++ b/tests/fixtures/v3/security_groups/POST_response.json @@ -0,0 +1,44 @@ +{ + "guid": "b85a788e-671f-4549-814d-e34cdb2f539a", + "created_at": "2020-02-20T17:42:08Z", + "updated_at": "2020-02-20T17:42:08Z", + "name": "my-group0", + "globally_enabled": { + "running": true, + "staging": false + }, + "rules": [ + { + "protocol": "tcp", + "destination": "10.10.10.0/24", + "ports": "443,80,8080" + }, + { + "protocol": "icmp", + "destination": "10.10.10.0/24", + "type": 8, + "code": 0, + "description": "Allow ping requests to private services" + } + ], + "relationships": { + "staging_spaces": { + "data": [ + { + "guid": "space-guid-1" + }, + { + "guid": "space-guid-2" + } + ] + }, + "running_spaces": { + "data": [] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/b85a788e-671f-4549-814d-e34cdb2f539a" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/security_groups/POST_{id}_relationships_running_spaces_response.json b/tests/fixtures/v3/security_groups/POST_{id}_relationships_running_spaces_response.json new file mode 100644 index 0000000..a299de4 --- /dev/null +++ b/tests/fixtures/v3/security_groups/POST_{id}_relationships_running_spaces_response.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "guid": "space-guid1" + }, + { + "guid": "space-guid2" + }, + { + "guid": "previous-space-guid" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/b85a788e-671f-4549-814d-e34cdb2f539a/relationships/running_spaces" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/security_groups/POST_{id}_relationships_staging_spaces_response.json b/tests/fixtures/v3/security_groups/POST_{id}_relationships_staging_spaces_response.json new file mode 100644 index 0000000..2a5760a --- /dev/null +++ b/tests/fixtures/v3/security_groups/POST_{id}_relationships_staging_spaces_response.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "guid": "space-guid1" + }, + { + "guid": "space-guid2" + }, + { + "guid": "previous-space-guid" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/b85a788e-671f-4549-814d-e34cdb2f539a/relationships/staging_spaces" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_brokers/GET_response.json b/tests/fixtures/v3/service_brokers/GET_response.json new file mode 100644 index 0000000..bfd4bb0 --- /dev/null +++ b/tests/fixtures/v3/service_brokers/GET_response.json @@ -0,0 +1,78 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "http://somewhere.org/v3/service_brokers?page=1&per_page=1" + }, + "last": { + "href": "http://somewhere.org/v3/service_brokers?page=2&per_page=1" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "name": "my_service_broker", + "url": "https://example.service-broker.com", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "metadata": { + "labels": { + "type": "dev" + }, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_brokers/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_offerings": { + "href": "https://api.example.org/v3/service_offerings?service_broker_guids=dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + { + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c4", + "name": "my_service_broker2", + "url": "https://example.service-broker.com", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "metadata": { + "labels": { + "type": "dev" + }, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_brokers/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_offerings": { + "href": "https://api.example.org/v3/service_offerings?service_broker_guids=dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_brokers/GET_{id}_response.json b/tests/fixtures/v3/service_brokers/GET_{id}_response.json new file mode 100644 index 0000000..f6a7b1c --- /dev/null +++ b/tests/fixtures/v3/service_brokers/GET_{id}_response.json @@ -0,0 +1,31 @@ +{ + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "name": "my_service_broker", + "url": "https://example.service-broker.com", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "metadata": { + "labels": { + "type": "dev" + }, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_brokers/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_offerings": { + "href": "https://api.example.org/v3/service_offerings?service_broker_guids=dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_brokers/PATCH_{id}_response.json b/tests/fixtures/v3/service_brokers/PATCH_{id}_response.json new file mode 100644 index 0000000..4414d34 --- /dev/null +++ b/tests/fixtures/v3/service_brokers/PATCH_{id}_response.json @@ -0,0 +1,31 @@ +{ + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "name": "my_service_broker", + "url": "https://example.service-broker.com", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "metadata": { + "labels": { + "hello": "world" + }, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_brokers/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_offerings": { + "href": "https://api.example.org/v3/service_offerings?service_broker_guids=dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_brokers/POST_response.json b/tests/fixtures/v3/service_brokers/POST_response.json new file mode 100644 index 0000000..63f22a6 --- /dev/null +++ b/tests/fixtures/v3/service_brokers/POST_response.json @@ -0,0 +1,31 @@ +{ + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "name": "my_service_broker_renamed", + "url": "https://example.service-broker.com", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } + }, + "metadata": { + "labels": { + "type": "dev" + }, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_brokers/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "service_offerings": { + "href": "https://api.example.org/v3/service_offerings?service_broker_guids=dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_credential_bindings/GET_response.json b/tests/fixtures/v3/service_credential_bindings/GET_response.json new file mode 100644 index 0000000..108f612 --- /dev/null +++ b/tests/fixtures/v3/service_credential_bindings/GET_response.json @@ -0,0 +1,100 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/service_credential_bindings?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/service_credential_bindings?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "name": "some-binding-name", + "type": "app", + "last_operation": { + "type": "create", + "state": "succeeded", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z" + }, + "metadata": { + "annotations": { + "foo": "bar" + }, + "labels": { + "baz": "qux" + } + }, + "relationships": { + "app": { + "data": { + "guid": "74f7c078-0934-470f-9883-4fddss5b8f13" + } + }, + "service_instance": { + "data": { + "guid": "8bfe4c1b-9e18-45b1-83be-124163f31f9e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "details": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/details" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/8bfe4c1b-9e18-45b1-83be-124163f31f9e" + }, + "app": { + "href": "https://api.example.org/v3/apps/74f7c078-0934-470f-9883-4fddss5b8f13" + } + } + }, + { + "guid": "7aa37bad-6ccb-4ef9-ba48-9ce3a91b2b62", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "name": "some-key-name", + "type": "key", + "last_operation": { + "type": "create", + "state": "succeeded", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z" + }, + "metadata": { + "annotations": { + "foo": "bar" + }, + "labels": { } + }, + "relationships": { + "service_instance": { + "data": { + "guid": "8bfe4c1b-9e18-45b1-83be-124163f31f9e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_credential_bindings/7aa37bad-6ccb-4ef9-ba48-9ce3a91b2b62" + }, + "details": { + "href": "https://api.example.org/v3/service_credential_bindings/7aa37bad-6ccb-4ef9-ba48-9ce3a91b2b62/details" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/8bf356j3-9e18-45b1-3333-124163f31f9e" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_credential_bindings/GET_{id}_details_response.json b/tests/fixtures/v3/service_credential_bindings/GET_{id}_details_response.json new file mode 100644 index 0000000..415f896 --- /dev/null +++ b/tests/fixtures/v3/service_credential_bindings/GET_{id}_details_response.json @@ -0,0 +1,10 @@ +{ + "credentials": { + "connection": "mydb://user@password:example.com" + }, + "syslog_drain_url": "http://syslog.example.com/drain", + "volume_mounts": [ + "/vcap/data", + "store" + ] +} diff --git a/tests/fixtures/v3/service_credential_bindings/GET_{id}_parameters_response.json b/tests/fixtures/v3/service_credential_bindings/GET_{id}_parameters_response.json new file mode 100644 index 0000000..4fddbe3 --- /dev/null +++ b/tests/fixtures/v3/service_credential_bindings/GET_{id}_parameters_response.json @@ -0,0 +1,4 @@ +{ + "foo": "bar", + "foz": "baz" +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_credential_bindings/GET_{id}_response.json b/tests/fixtures/v3/service_credential_bindings/GET_{id}_response.json new file mode 100644 index 0000000..d9a4e38 --- /dev/null +++ b/tests/fixtures/v3/service_credential_bindings/GET_{id}_response.json @@ -0,0 +1,50 @@ +{ + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "name": "some-name", + "type": "app", + "last_operation": { + "type": "create", + "state": "succeeded", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z" + }, + "metadata": { + "annotations": { + "foo": "bar" + }, + "labels": { + "baz": "qux" + } + }, + "relationships": { + "app": { + "data": { + "guid": "74f7c078-0934-470f-9883-4fddss5b8f13" + } + }, + "service_instance": { + "data": { + "guid": "8bfe4c1b-9e18-45b1-83be-124163f31f9e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "details": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/details" + }, + "parameters": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/parameters" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/8bfe4c1b-9e18-45b1-83be-124163f31f9e" + }, + "app": { + "href": "https://api.example.org/v3/apps/74f7c078-0934-470f-9883-4fddss5b8f13" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_credential_bindings/POST_response.json b/tests/fixtures/v3/service_credential_bindings/POST_response.json new file mode 100644 index 0000000..d8b2089 --- /dev/null +++ b/tests/fixtures/v3/service_credential_bindings/POST_response.json @@ -0,0 +1,50 @@ +{ + "guid": "dde5ad2a-d8f4-44dc-a56f-0452d744f1c3", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "name": "some-name", + "type": "app", + "last_operation": { + "type": "create", + "state": "succeeded", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z" + }, + "metadata": { + "annotations": { + "foo": "bar" + }, + "labels": { + "baz": "qux" + } + }, + "relationships": { + "app": { + "data": { + "guid": "74f7c078-0934-470f-9883-4fddss5b8f13" + } + }, + "service_instance": { + "data": { + "guid": "8bfe4c1b-9e18-45b1-83be-124163f31f9e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3" + }, + "details": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/details" + }, + "parameters": { + "href": "https://api.example.org/v3/service_credential_bindings/dde5ad2a-d8f4-44dc-a56f-0452d744f1c3/parameters" + }, + "service_instance": { + "href": "https://api.example.org/v3/service_instances/8bfe4c1b-9e18-45b1-83be-124163f31f9e" + }, + "app": { + "href": "https://api.example.org/v3/apps/74f7c078-0934-470f-9883-4fddss5b8f13" + } + } +} diff --git a/tests/fixtures/v3/service_instances/GET_response.json b/tests/fixtures/v3/service_instances/GET_response.json new file mode 100644 index 0000000..51e5d0b --- /dev/null +++ b/tests/fixtures/v3/service_instances/GET_response.json @@ -0,0 +1,61 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/service_instances?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/service_instances?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "88ce23e5-27c3-4381-a2df-32a28ec43133", + "created_at": "2020-03-10T15:56:08Z", + "updated_at": "2020-03-10T15:56:08Z", + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "Operation succeeded", + "updated_at": "2020-03-10T15:49:32Z", + "created_at": "2020-03-10T15:49:29Z" + }, + "name": "my-user-provided-instance", + "tags": ["sql"], + "type": "user-provided", + "syslog_drain_url": "http://logs.com", + "route_service_url": "https://routes.com", + "relationships": { + "space": { + "data": { + "guid": "5a84d315-9513-4d74-95e5-f6a5501eeef7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_instances/88ce23e5-27c3-4381-a2df-32a28ec43133" + }, + "space": { + "href": "https://api.example.org/v3/spaces/5a84d315-9513-4d74-95e5-f6a5501eeef7" + }, + "credentials": { + "href": "https://api.example.org/v3/service_instances/88ce23e5-27c3-4381-a2df-32a28ec43133/credentials" + }, + "service_credential_bindings": { + "href": "https://api.example.org/v3/service_credential_bindings?service_instance_guids=88ce23e5-27c3-4381-a2df-32a28ec43133" + }, + "service_route_bindings": { + "href": "https://api.example.org/v3/service_route_bindings?service_instance_guids=88ce23e5-27c3-4381-a2df-32a28ec43133" + } + } + } + ] +} \ No newline at end of file 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}_credentials_response.json b/tests/fixtures/v3/service_instances/GET_{id}_credentials_response.json new file mode 100644 index 0000000..36e4a89 --- /dev/null +++ b/tests/fixtures/v3/service_instances/GET_{id}_credentials_response.json @@ -0,0 +1,5 @@ +{ + "username": "my-username", + "password": "super-secret", + "other": "credential" +} diff --git a/tests/fixtures/v3/service_instances/GET_{id}_permissions_response.json b/tests/fixtures/v3/service_instances/GET_{id}_permissions_response.json new file mode 100644 index 0000000..d7001ba --- /dev/null +++ b/tests/fixtures/v3/service_instances/GET_{id}_permissions_response.json @@ -0,0 +1,4 @@ +{ + "read": true, + "manage": false +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_instances/GET_{id}_response.json b/tests/fixtures/v3/service_instances/GET_{id}_response.json new file mode 100644 index 0000000..6a476fc --- /dev/null +++ b/tests/fixtures/v3/service_instances/GET_{id}_response.json @@ -0,0 +1,45 @@ +{ + "guid": "service_instance_id", + "created_at": "2020-03-10T15:56:08Z", + "updated_at": "2020-03-10T15:56:08Z", + "last_operation": { + "type": "create", + "state": "succeeded", + "description": "Operation succeeded", + "updated_at": "2020-03-10T15:49:32Z", + "created_at": "2020-03-10T15:49:29Z" + }, + "name": "my-user-provided-instance", + "tags": ["sql"], + "type": "user-provided", + "syslog_drain_url": "http://logs.com", + "route_service_url": "https://routes.com", + "relationships": { + "space": { + "data": { + "guid": "5a84d315-9513-4d74-95e5-f6a5501eeef7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_instances/service_instance_id" + }, + "space": { + "href": "https://api.example.org/v3/spaces/5a84d315-9513-4d74-95e5-f6a5501eeef7" + }, + "credentials": { + "href": "https://api.example.org/v3/service_instances/service_instance_id/credentials" + }, + "service_credential_bindings": { + "href": "https://api.example.org/v3/service_credential_bindings?service_instance_guids=service_instance_id" + }, + "service_route_bindings": { + "href": "https://api.example.org/v3/service_route_bindings?service_instance_guids=service_instance_id" + } + } +} \ 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/fixtures/v3/service_offerings/GET_response.json b/tests/fixtures/v3/service_offerings/GET_response.json new file mode 100644 index 0000000..24420e8 --- /dev/null +++ b/tests/fixtures/v3/service_offerings/GET_response.json @@ -0,0 +1,109 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/service_offerings?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/service_offerings?page=1&per_page=50" + }, + "previous": null + }, + "resources": [ + { + "guid": "bf7eb420-11e5-11ea-b7db-4b5d5e7976a9", + "name": "my_service_offering", + "description": "Provides my service", + "available": true, + "tags": ["relational", "caching"], + "requires": [], + "created_at": "2019-11-28T13:44:02Z", + "updated_at": "2019-11-28T13:44:02Z", + "shareable": true, + "documentation_url": "https://some-documentation-link.io", + "broker_catalog": { + "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", + "metadata": { + "shareable": true + }, + "features": { + "plan_updateable": true, + "bindable": true, + "instances_retrievable": true, + "bindings_retrievable": true, + "allow_context_updates": false + } + }, + "relationships": { + "service_broker": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_offerings/bf7eb420-11e5-11ea-b7db-4b5d5e7976a" + }, + "service_plans": { + "href": "https://api.example.org/v3/service_plans?service_offering_guids=bf7eb420-11e5-11ea-b7db-4b5d5e7976a" + }, + "service_broker": { + "href": "https://api.example.org/v3/service_brokers/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + { + "guid": "20e6cd62-12bb-11ea-90d1-7bfec2c75bcd", + "name": "other_service_offering", + "description": "Provides another service", + "available": true, + "tags": ["caching"], + "requires": [], + "created_at": "2019-11-29T16:44:02Z", + "updated_at": "2019-11-29T16:44:02Z", + "shareable": true, + "documentation_url": "https://some-other-documentation-link.io", + "broker_catalog": { + "id": "3cb11822-12bb-11ea-beb1-a350dc7453b9", + "metadata": { + "shareable": true + }, + "features": { + "plan_updateable": true, + "bindable": true, + "instances_retrievable": true, + "bindings_retrievable": true, + "allow_context_updates": false + } + }, + "relationships": { + "service_broker": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_offerings/20e6cd62-12bb-11ea-90d1-7bfec2c75bcd" + }, + "service_plans": { + "href": "https://api.example.org/v3/service_plans?service_offering_guids=20e6cd62-12bb-11ea-90d1-7bfec2c75bcd" + }, + "service_broker": { + "href": "https://api.example.org/v3/service_brokers/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_offerings/GET_{id}_response.json b/tests/fixtures/v3/service_offerings/GET_{id}_response.json new file mode 100644 index 0000000..41c9901 --- /dev/null +++ b/tests/fixtures/v3/service_offerings/GET_{id}_response.json @@ -0,0 +1,47 @@ +{ + "guid": "service_offering_guid", + "name": "my_service_offering", + "description": "Provides my service", + "available": true, + "tags": ["relational", "caching"], + "requires": [], + "created_at": "2019-11-28T13:44:02Z", + "updated_at": "2019-11-28T13:44:02Z", + "shareable": true, + "documentation_url": "https://some-documentation-link.io", + "broker_catalog": { + "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", + "metadata": { + "shareable": true + }, + "features": { + "plan_updateable": true, + "bindable": true, + "instances_retrievable": true, + "bindings_retrievable": true, + "allow_context_updates": false + } + }, + "relationships": { + "service_broker": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_offerings/bf7eb420-11e5-11ea-b7db-4b5d5e7976a" + }, + "service_plans": { + "href": "https://api.example.org/v3/service_plans?service_offering_guids=bf7eb420-11e5-11ea-b7db-4b5d5e7976a" + }, + "service_broker": { + "href": "https://api.example.org/v3/service_brokers/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } +} diff --git a/tests/fixtures/v3/service_offerings/PATCH_{id}_response.json b/tests/fixtures/v3/service_offerings/PATCH_{id}_response.json new file mode 100644 index 0000000..1027c41 --- /dev/null +++ b/tests/fixtures/v3/service_offerings/PATCH_{id}_response.json @@ -0,0 +1,49 @@ +{ + "guid": "service_offering_guid", + "name": "my_service_offering", + "description": "Provides my service", + "available": true, + "tags": ["relational", "caching"], + "requires": [], + "created_at": "2019-11-28T13:44:02Z", + "updated_at": "2019-11-28T13:44:02Z", + "shareable": true, + "documentation_url": "https://some-documentation-link.io", + "broker_catalog": { + "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", + "metadata": { + "shareable": true + }, + "features": { + "plan_updateable": true, + "bindable": true, + "instances_retrievable": true, + "bindings_retrievable": true, + "allow_context_updates": false + } + }, + "relationships": { + "service_broker": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": { + "hello": "world" + }, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_offerings/bf7eb420-11e5-11ea-b7db-4b5d5e7976a" + }, + "service_plans": { + "href": "https://api.example.org/v3/service_plans?service_offering_guids=bf7eb420-11e5-11ea-b7db-4b5d5e7976a" + }, + "service_broker": { + "href": "https://api.example.org/v3/service_brokers/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } +} diff --git a/tests/fixtures/v3/service_plans/GET_response.json b/tests/fixtures/v3/service_plans/GET_response.json new file mode 100644 index 0000000..267d524 --- /dev/null +++ b/tests/fixtures/v3/service_plans/GET_response.json @@ -0,0 +1,152 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/service_plans?page=1&per_page=1" + }, + "last": { + "href": "https://api.example.org/v3/service_plans?page=2&per_page=1" + }, + "previous": null + }, + "resources": [ + { + "guid": "bf7eb420-11e5-11ea-b7db-4b5d5e7976a9", + "name": "my_big_service_plan", + "description": "Big plan", + "visibility_type": "organization", + "available": true, + "free": false, + "costs": [ + { + "currency": "USD", + "amount": 199.99, + "unit": "Monthly" + } + ], + "created_at": "2019-11-28T13:44:02Z", + "updated_at": "2019-11-28T13:44:02Z", + "maintenance_info": { + "version": "1.0.0+dev4", + "description": "Database version 7.8.0" + }, + "broker_catalog": { + "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", + "metadata": { + "custom-key": "custom-value" + }, + "maximum_polling_duration": null, + "features": { + "plan_updateable": true, + "bindable": true + } + }, + "schemas": { + "service_instance": { + "create": { + "parameters": { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "billing-account": { + "description": "Billing account number used to charge use of shared fake server.", + "type": "string" + } + } + } + }, + "update": { + "parameters": {} + } + }, + "service_binding": { + "create": { + "parameters": {} + } + } + }, + "relationships": { + "service_offering": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_plans/bf7eb420-11e5-11ea-b7db-4b5d5e7976a9" + }, + "service_offering": { + "href": "https://api.example.org/v3/service_offerings/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + }, + "visibility": { + "href": "https://api.example.org/v3/service_plans/bf7eb420-11e5-11ea-b7db-4b5d5e7976a9/visibility" + } + } + }, + { + "guid": "20e6cd62-12bb-11ea-90d1-7bfec2c75bcd", + "name": "other_service_plan", + "description": "Provides another service plan", + "visibility_type": "admin", + "available": true, + "free": true, + "created_at": "2019-11-29T16:44:02Z", + "updated_at": "2019-11-29T16:44:02Z", + "maintenance_info": {}, + "broker_catalog": { + "id": "3cb11822-12bb-11ea-beb1-a350dc7453b9", + "metadata": { + "other-data": true + }, + "maximum_polling_duration": null, + "features": { + "plan_updateable": true, + "bindable": true + } + }, + "schemas": { + "service_instance": { + "create": { + "parameters": {} + }, + "update": { + "parameters": {} + } + }, + "service_binding": { + "create": { + "parameters": {} + } + } + }, + "relationships": { + "service_offering": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_plans/20e6cd62-12bb-11ea-90d1-7bfec2c75bcd" + }, + "service_offering": { + "href": "https://api.example.org/v3/service_offerings/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + }, + "visibility": { + "href": "https://api.example.org/v3/service_plans/20e6cd62-12bb-11ea-90d1-7bfec2c75bcd/visibility" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_plans/GET_{id}_response.json b/tests/fixtures/v3/service_plans/GET_{id}_response.json new file mode 100644 index 0000000..a2473d9 --- /dev/null +++ b/tests/fixtures/v3/service_plans/GET_{id}_response.json @@ -0,0 +1,78 @@ +{ + "guid": "bf7eb420-11e5-11ea-b7db-4b5d5e7976a9", + "name": "my_service_plan", + "description": "Big", + "visibility_type": "public", + "available": true, + "free": false, + "costs": [ + { + "currency": "USD", + "amount": 199.99, + "unit": "Monthly" + } + ], + "created_at": "2019-11-28T13:44:02Z", + "updated_at": "2019-11-28T13:44:02Z", + "maintenance_info": { + "version": "1.0.0+dev4", + "description": "Database version 7.8.0" + }, + "broker_catalog": { + "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", + "metadata": { + "custom-key": "custom-information" + }, + "maximum_polling_duration": null, + "features": { + "plan_updateable": true, + "bindable": true + } + }, + "schemas": { + "service_instance": { + "create": { + "parameters": { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "billing-account": { + "description": "Billing account number used to charge use of shared fake server.", + "type": "string" + } + } + } + }, + "update": { + "parameters": {} + } + }, + "service_binding": { + "create": { + "parameters": {} + } + } + }, + "relationships": { + "service_offering": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_plans/bf7eb420-11e5-11ea-b7db-4b5d5e7976a9" + }, + "service_offering": { + "href": "https://api.example.org/v3/service_offerings/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + }, + "visibility": { + "href": "https://api.example.org/v3/service_plans/bf7eb420-11e5-11ea-b7db-4b5d5e7976a9/visibility" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_plans/GET_{id}_visibility_response.json b/tests/fixtures/v3/service_plans/GET_{id}_visibility_response.json new file mode 100644 index 0000000..8c6f883 --- /dev/null +++ b/tests/fixtures/v3/service_plans/GET_{id}_visibility_response.json @@ -0,0 +1,3 @@ +{ + "type": "public" +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_plans/PATCH_{id}_response.json b/tests/fixtures/v3/service_plans/PATCH_{id}_response.json new file mode 100644 index 0000000..36e342f --- /dev/null +++ b/tests/fixtures/v3/service_plans/PATCH_{id}_response.json @@ -0,0 +1,84 @@ +{ + "guid": "bf7eb420-11e5-11ea-b7db-4b5d5e7976a9", + "name": "my_service_plan", + "description": "Big", + "visibility_type": "public", + "available": true, + "free": false, + "costs": [ + { + "currency": "USD", + "amount": 199.99, + "unit": "Monthly" + } + ], + "created_at": "2019-11-28T13:44:02Z", + "updated_at": "2019-11-28T13:44:02Z", + "maintenance_info": { + "version": "1.0.0+dev4", + "description": "Database version 7.8.0" + }, + "broker_catalog": { + "id": "db730a8c-11e5-11ea-838a-0f4fff3b1cfb", + "metadata": { + "custom-key": "custom-information" + }, + "maximum_polling_duration": null, + "features": { + "plan_updateable": true, + "bindable": true + } + }, + "schemas": { + "service_instance": { + "create": { + "parameters": { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "billing-account": { + "description": "Billing account number used to charge use of shared fake server.", + "type": "string" + } + } + } + }, + "update": { + "parameters": {} + } + }, + "service_binding": { + "create": { + "parameters": {} + } + } + }, + "relationships": { + "service_offering": { + "data": { + "guid": "13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + } + } + }, + "metadata": { + "labels": { + "hello": "world" + }, + "annotations": { + "annotations": { + "note": "detailed information" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/service_plans/bf7eb420-11e5-11ea-b7db-4b5d5e7976a9" + }, + "service_offering": { + "href": "https://api.example.org/v3/service_offerings/13c60e38-11e7-11ea-9106-33ee3c5bd4d7" + }, + "visibility": { + "href": "https://api.example.org/v3/service_plans/bf7eb420-11e5-11ea-b7db-4b5d5e7976a9/visibility" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_plans/PATCH_{id}_visibility_organizations_response.json b/tests/fixtures/v3/service_plans/PATCH_{id}_visibility_organizations_response.json new file mode 100644 index 0000000..dcdda7d --- /dev/null +++ b/tests/fixtures/v3/service_plans/PATCH_{id}_visibility_organizations_response.json @@ -0,0 +1,13 @@ +{ + "type": "organization", + "organizations": [ + { + "guid": "0fc1ad4f-e1d7-4436-8e23-6b20f03c6482", + "name": "some_org" + }, + { + "guid": "0fc1ad4f-e1d7-4436-8e23-6b20f03c6483", + "name": "other_org" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_plans/PATCH_{id}_visibility_type_response.json b/tests/fixtures/v3/service_plans/PATCH_{id}_visibility_type_response.json new file mode 100644 index 0000000..f0f8c79 --- /dev/null +++ b/tests/fixtures/v3/service_plans/PATCH_{id}_visibility_type_response.json @@ -0,0 +1,3 @@ +{ + "type": "admin" +} \ No newline at end of file diff --git a/tests/fixtures/v3/service_plans/POST_{id}_visibility_response.json b/tests/fixtures/v3/service_plans/POST_{id}_visibility_response.json new file mode 100644 index 0000000..94c4fa4 --- /dev/null +++ b/tests/fixtures/v3/service_plans/POST_{id}_visibility_response.json @@ -0,0 +1,8 @@ +{ + "organizations": [ + { + "guid": "b3af3658-d844-496a-8986-89b79a74c8ae" + } + ], + "type": "organization" + } \ No newline at end of file diff --git a/test/fixtures/v3/spaces/GET_response.json b/tests/fixtures/v3/spaces/GET_response.json similarity index 100% rename from test/fixtures/v3/spaces/GET_response.json rename to tests/fixtures/v3/spaces/GET_response.json diff --git a/tests/fixtures/v3/spaces/GET_response_include_org.json b/tests/fixtures/v3/spaces/GET_response_include_org.json new file mode 100644 index 0000000..b52493d --- /dev/null +++ b/tests/fixtures/v3/spaces/GET_response_include_org.json @@ -0,0 +1,96 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "http://somewhere.org/v3/spaces?page=1&per_page=50" + }, + "last": { + "href": "http://somewhere.org/v3/spaces?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "space1", + "relationships": { + "organization": { + "data": { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/spaces/885735b5-aea4-4cf5-8e44-961af0e41920" + }, + "organization": { + "href": "http://somewhere.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {} + } + }, + { + "guid": "d4c91047-7b29-4fda-b7f9-04033e5c9c9f", + "created_at": "2017-02-02T00:14:30Z", + "updated_at": "2017-02-02T00:14:30Z", + "name": "space2", + "relationships": { + "organization": { + "data": { + "guid": "b4ce91bd-31df-4b7d-8fd4-21a6b533276b" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/spaces/d4c91047-7b29-4fda-b7f9-04033e5c9c9f" + }, + "organization": { + "href": "http://somewhere.org/v3/organizations/b4ce91bd-31df-4b7d-8fd4-21a6b533276b" + } + }, + "metadata": { + "labels": {} + } + } + ], + "included": { + "organizations": [ + { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "org1", + "links": { + "self": { + "href": "http://somewhere.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {} + } + }, + { + "guid": "b4ce91bd-31df-4b7d-8fd4-21a6b533276b", + "created_at": "2017-02-02T00:14:30Z", + "updated_at": "2017-02-02T00:14:30Z", + "name": "org2", + "links": { + "self": { + "href": "http://somewhere.org/v3/organizations/b4ce91bd-31df-4b7d-8fd4-21a6b533276b" + } + }, + "metadata": { + "labels": {} + } + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/spaces/GET_{id}_relationships_isolation_segment_response.json b/tests/fixtures/v3/spaces/GET_{id}_relationships_isolation_segment_response.json new file mode 100644 index 0000000..eb2873b --- /dev/null +++ b/tests/fixtures/v3/spaces/GET_{id}_relationships_isolation_segment_response.json @@ -0,0 +1,14 @@ +{ + "data": { + "guid": "e4c91047-3b29-4fda-b7f9-04033e5a9c9f" + }, + "links": { + "self": { + "href": "https://api.example.org/v3/spaces/885735b5-aea4-4cf5-8e44-961af0e41920/relationships/isolation_segment" + }, + "related": { + "href": "https://api.example.org/v3/isolation_segments/e4c91047-3b29-4fda-b7f9-04033e5a9c9f" + } + } +} + diff --git a/test/fixtures/v3/spaces/GET_{id}_response.json b/tests/fixtures/v3/spaces/GET_{id}_response.json similarity index 100% rename from test/fixtures/v3/spaces/GET_{id}_response.json rename to tests/fixtures/v3/spaces/GET_{id}_response.json diff --git a/tests/fixtures/v3/spaces/GET_{id}_response_include_org.json b/tests/fixtures/v3/spaces/GET_{id}_response_include_org.json new file mode 100644 index 0000000..d5c51c5 --- /dev/null +++ b/tests/fixtures/v3/spaces/GET_{id}_response_include_org.json @@ -0,0 +1,42 @@ +{ + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my-space", + "relationships": { + "organization": { + "data": { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133" + } + } + }, + "links": { + "self": { + "href": "http://somewhere.org/v3/spaces/885735b5-aea4-4cf5-8e44-961af0e41920" + }, + "organization": { + "href": "http://somewhere.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {} + }, + "included": { + "organizations": [ + { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my-organization", + "links": { + "self": { + "href": "http://somewhere.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {} + } + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/spaces/POST_response.json b/tests/fixtures/v3/spaces/POST_response.json new file mode 100644 index 0000000..430279b --- /dev/null +++ b/tests/fixtures/v3/spaces/POST_response.json @@ -0,0 +1,28 @@ +{ + "guid": "885735b5-aea4-4cf5-8e44-961af0e41920", + "created_at": "2017-02-01T01:33:58Z", + "updated_at": "2017-02-01T01:33:58Z", + "name": "my-space", + "relationships": { + "organization": { + "data": { + "guid": "e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "quota": { + "data": null + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/spaces/885735b5-aea4-4cf5-8e44-961af0e41920" + }, + "organization": { + "href": "https://api.example.org/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/spaces/POST_{id}_relationships_isolation_segment_response.json b/tests/fixtures/v3/spaces/POST_{id}_relationships_isolation_segment_response.json new file mode 100644 index 0000000..94af7fc --- /dev/null +++ b/tests/fixtures/v3/spaces/POST_{id}_relationships_isolation_segment_response.json @@ -0,0 +1,5 @@ +{ + "data": { + "guid": "iso-seg-guid" + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/stacks/GET_response.json b/tests/fixtures/v3/stacks/GET_response.json new file mode 100644 index 0000000..10aaa83 --- /dev/null +++ b/tests/fixtures/v3/stacks/GET_response.json @@ -0,0 +1,54 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/stacks?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/stacks?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack-1", + "build_rootfs_image": "my-stack-1-build", + "run_rootfs_image": "my-stack-1-run", + "description": "This is my first stack!", + "default": true, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } + }, + { + "guid": "81c916c9-c2f9-440e-8e73-102e79c4704h", + "created_at": "2018-11-09T22:43:29Z", + "updated_at": "2018-11-09T22:43:29Z", + "name": "my-stack-2", + "description": "This is my second stack!", + "build_rootfs_image": "my-stack-2-build", + "run_rootfs_image": "my-stack-2-run", + "default": false, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/stacks/81c916c9-c2f9-440e-8e73-102e79c4704h" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/stacks/GET_{id}_apps_response.json b/tests/fixtures/v3/stacks/GET_{id}_apps_response.json new file mode 100644 index 0000000..a542fa6 --- /dev/null +++ b/tests/fixtures/v3/stacks/GET_{id}_apps_response.json @@ -0,0 +1,162 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/stacks/[guid]/apps?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/stacks/[guid]/apps?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446", + "name": "my_app", + "state": "STARTED", + "created_at": "2016-03-17T21:41:30Z", + "updated_at": "2016-03-18T11:32:30Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": ["java_buildpack"], + "stack": "cflinuxfs4" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + }, + "current_droplet": { + "data": { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/processes" + }, + "packages": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/packages" + }, + "environment_variables": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/environment_variables" + }, + "current_droplet": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets/current" + }, + "droplets": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets" + }, + "tasks": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/tasks" + }, + "start": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/start", + "method": "POST" + }, + "stop": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/stop", + "method": "POST" + }, + "revisions": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions" + }, + "deployed_revisions": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions/deployed" + }, + "features": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/features" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "02b4ec9b-94c7-4468-9c23-4e906191a0f8", + "name": "my_app2", + "state": "STOPPED", + "created_at": "1970-01-01T00:00:02Z", + "updated_at": "2016-06-08T16:41:26Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": ["ruby_buildpack"], + "stack": "cflinuxfs4" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + }, + "droplet": { + "data": { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/processes" + }, + "packages": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/packages" + }, + "environment_variables": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/environment_variables" + }, + "current_droplet": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets/current" + }, + "droplets": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets" + }, + "tasks": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/tasks" + }, + "start": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/start", + "method": "POST" + }, + "stop": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/stop", + "method": "POST" + }, + "revisions": { + "href": "https://api.example.org//v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/revisions" + }, + "deployed_revisions": { + "href": "https://api.example.org//v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/revisions/deployed" + }, + "features": { + "href": "https://api.example.org//v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/features" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] + } \ No newline at end of file diff --git a/tests/fixtures/v3/stacks/GET_{id}_response.json b/tests/fixtures/v3/stacks/GET_{id}_response.json new file mode 100644 index 0000000..b6a38b4 --- /dev/null +++ b/tests/fixtures/v3/stacks/GET_{id}_response.json @@ -0,0 +1,19 @@ +{ + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack", + "description": "Here is my stack!", + "build_rootfs_image": "my-stack", + "run_rootfs_image": "my-stack", + "default": true, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.com/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } +} diff --git a/tests/fixtures/v3/stacks/PATCH_{id}_response.json b/tests/fixtures/v3/stacks/PATCH_{id}_response.json new file mode 100644 index 0000000..f2e2f01 --- /dev/null +++ b/tests/fixtures/v3/stacks/PATCH_{id}_response.json @@ -0,0 +1,19 @@ +{ + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack", + "description": "Here is my stack!", + "build_rootfs_image": "my-stack", + "run_rootfs_image": "my-stack", + "default": true, + "metadata": { + "labels": {"key":"value"}, + "annotations": {"note":"detailed information"} + }, + "links": { + "self": { + "href": "https://api.example.com/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } +} diff --git a/tests/fixtures/v3/stacks/POST_response.json b/tests/fixtures/v3/stacks/POST_response.json new file mode 100644 index 0000000..80cce02 --- /dev/null +++ b/tests/fixtures/v3/stacks/POST_response.json @@ -0,0 +1,19 @@ +{ + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack", + "description": "Here is my stack!", + "build_rootfs_image": "my-stack", + "run_rootfs_image": "my-stack", + "default": true, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.com/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } +} \ No newline at end of file diff --git a/test/fixtures/v3/tasks/GET_response.json b/tests/fixtures/v3/tasks/GET_response.json similarity index 100% rename from test/fixtures/v3/tasks/GET_response.json rename to tests/fixtures/v3/tasks/GET_response.json diff --git a/test/fixtures/v3/tasks/GET_{id}_response.json b/tests/fixtures/v3/tasks/GET_{id}_response.json similarity index 100% rename from test/fixtures/v3/tasks/GET_{id}_response.json rename to tests/fixtures/v3/tasks/GET_{id}_response.json diff --git a/test/fixtures/v3/tasks/POST_response.json b/tests/fixtures/v3/tasks/POST_response.json similarity index 100% rename from test/fixtures/v3/tasks/POST_response.json rename to tests/fixtures/v3/tasks/POST_response.json diff --git a/test/fixtures/v3/tasks/POST_{id}_actions_cancel_response.json b/tests/fixtures/v3/tasks/POST_{id}_actions_cancel_response.json similarity index 100% rename from test/fixtures/v3/tasks/POST_{id}_actions_cancel_response.json rename to tests/fixtures/v3/tasks/POST_{id}_actions_cancel_response.json diff --git a/tests/fixtures/v3/users/GET_response.json b/tests/fixtures/v3/users/GET_response.json new file mode 100644 index 0000000..de19877 --- /dev/null +++ b/tests/fixtures/v3/users/GET_response.json @@ -0,0 +1,50 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/users?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/users?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "client_id", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": null, + "presentation_name": "client_id", + "origin": null, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } + }, + { + "guid": "9da93b89-3f89-4f05-7238-8a2b123c79l9", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/9da93b89-3f89-4f05-7238-8a2b123c79l9" + } + } + } + ] +} diff --git a/tests/fixtures/v3/users/GET_{id}_response.json b/tests/fixtures/v3/users/GET_{id}_response.json new file mode 100644 index 0000000..b60ce5a --- /dev/null +++ b/tests/fixtures/v3/users/GET_{id}_response.json @@ -0,0 +1,17 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/users/PATCH_{id}_response.json b/tests/fixtures/v3/users/PATCH_{id}_response.json new file mode 100644 index 0000000..d516fb4 --- /dev/null +++ b/tests/fixtures/v3/users/PATCH_{id}_response.json @@ -0,0 +1,21 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": { + "enviroment": "production" + }, + "annotations": { + "note": "detailed information" + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} diff --git a/tests/fixtures/v3/users/POST_response.json b/tests/fixtures/v3/users/POST_response.json new file mode 100644 index 0000000..b60ce5a --- /dev/null +++ b/tests/fixtures/v3/users/POST_response.json @@ -0,0 +1,17 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} \ No newline at end of file diff --git a/test/v2/__init__.py b/tests/networking/__init__.py similarity index 100% rename from test/v2/__init__.py rename to tests/networking/__init__.py diff --git a/test/v3/__init__.py b/tests/networking/v1/__init__.py similarity index 100% rename from test/v3/__init__.py rename to tests/networking/v1/__init__.py diff --git a/tests/networking/v1/external/__init__.py b/tests/networking/v1/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/networking/v1/external/test_policies.py b/tests/networking/v1/external/test_policies.py new file mode 100644 index 0000000..581eb6b --- /dev/null +++ b/tests/networking/v1/external/test_policies.py @@ -0,0 +1,60 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.networking.v1.external.policies import Policy + + +class TestPolicies(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/networking/v1/external/policies", + HTTPStatus.OK, + None, + "networking", + "v1", + "external", + "policies", + "GET_response.json", + ) + all_policies = [policy for policy in self.client.networking_v1_external.policies.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_policies)) + self.assertEqual(all_policies[0]["source"]["id"], "1081ceac-f5c4-47a8-95e8-88e1e302efb5") + self.assertEqual(all_policies[0]["destination"]["id"], "38f08df0-19df-4439-b4e9-61096d4301ea") + + def test_delete(self): + self.client.delete.return_value = self.mock_response("/networking/v1/external/policies/delete", HTTPStatus.OK, None) + policy = Policy( + src_id="1081ceac-f5c4-47a8-95e8-88e1e302efb5", + dst_id="38f08df0-19df-4439-b4e9-61096d4301ea", + proto="tcp", + start_port=1234, + end_port=1234, + ) + self.client.networking_v1_external.policies.delete( + [ + policy, + ] + ) + self.client.delete.assert_called_with( + { + "policies": [ + { + "source": {"id": "1081ceac-f5c4-47a8-95e8-88e1e302efb5"}, + "destination": { + "id": "38f08df0-19df-4439-b4e9-61096d4301ea", + "ports": {"start": 1234, "end": 1234}, + "protocol": "tcp", + }, + } + ] + } + ) diff --git a/tests/operations/__init__.py b/tests/operations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/operations/push/__init__.py b/tests/operations/push/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/operations/push/test_cf_ignore.py b/tests/operations/push/test_cf_ignore.py similarity index 57% rename from test/operations/push/test_cf_ignore.py rename to tests/operations/push/test_cf_ignore.py index 6eb4f14..d418d7a 100644 --- a/test/operations/push/test_cf_ignore.py +++ b/tests/operations/push/test_cf_ignore.py @@ -1,52 +1,51 @@ import os from unittest import TestCase +from unittest.mock import mock_open, patch from cloudfoundry_client.operations.push.cf_ignore import CfIgnore -from imported import patch, mock_open, built_in_entry class TestCfIgnore(TestCase): - def test_open_cfignore_file(self): - with patch("%s.open" % built_in_entry, mock_open(read_data="*.log")) as mock_file, \ - patch('os.path.isfile', create=True) as mock_isfile: + with patch("builtins.open", mock_open(read_data="*.log")) as mock_file, patch( + "os.path.isfile", create=True + ) as mock_isfile: mock_isfile.__return_value__ = True - application_path = '/some/path' + application_path = "/some/path" CfIgnore(application_path) - mock_file.assert_called_with(os.path.join(application_path, '.cfignore'), 'r') + mock_file.assert_called_with(os.path.join(application_path, ".cfignore"), "r") def test_ignore_wildcard_resources(self): - with patch("%s.open" % built_in_entry, mock_open(read_data="*.log")), \ - patch('os.path.isfile', create=True) as mock_isfile: + with patch("builtins.open", mock_open(read_data="*.log")), patch("os.path.isfile", create=True) as mock_isfile: mock_isfile.__return_value__ = True - cf_ignore = CfIgnore('/some/path') + cf_ignore = CfIgnore("/some/path") self.assertTrue(cf_ignore.is_entry_ignored("toto.log")) self.assertTrue(cf_ignore.is_entry_ignored("/some/other/path/toto.log")) def test_ignore_directory(self): - with patch("%s.open" % built_in_entry, mock_open(read_data="ignored/directory/")), \ - patch('os.path.isfile', create=True) as mock_isfile: + with patch("builtins.open", mock_open(read_data="ignored/directory/")), patch( + "os.path.isfile", create=True + ) as mock_isfile: mock_isfile.__return_value__ = True - cf_ignore = CfIgnore('/some/path') + cf_ignore = CfIgnore("/some/path") self.assertTrue(cf_ignore.is_entry_ignored("ignored/directory/resource.file")) self.assertTrue(cf_ignore.is_entry_ignored("/ignored/directory/resource.file")) self.assertTrue(cf_ignore.is_entry_ignored("/some/sub/directory/containing/ignored/directory/resource.file")) # File in fact - self.assertFalse(cf_ignore.is_entry_ignored('/ignored/directory')) + self.assertFalse(cf_ignore.is_entry_ignored("/ignored/directory")) def test_ignore_file_with_directory(self): - with patch("%s.open" % built_in_entry, mock_open(read_data="ignored/directory/resource.file")), \ - patch('os.path.isfile', create=True) as mock_isfile: + with patch("builtins.open", mock_open(read_data="ignored/directory/resource.file")), patch( + "os.path.isfile", create=True + ) as mock_isfile: mock_isfile.__return_value__ = True - cf_ignore = CfIgnore('/some/path') + cf_ignore = CfIgnore("/some/path") self.assertTrue(cf_ignore.is_entry_ignored("ignored/directory/resource.file")) self.assertTrue(cf_ignore.is_entry_ignored("/ignored/directory/resource.file")) self.assertTrue(cf_ignore.is_entry_ignored("/some/sub/directory/containing/ignored/directory/resource.file")) # File in fact - self.assertFalse(cf_ignore.is_entry_ignored('ignored/resource.file')) - - + self.assertFalse(cf_ignore.is_entry_ignored("ignored/resource.file")) diff --git a/test/operations/push/test_file_helper.py b/tests/operations/push/test_file_helper.py similarity index 53% rename from test/operations/push/test_file_helper.py rename to tests/operations/push/test_file_helper.py index b21ca8a..fbf483e 100644 --- a/test/operations/push/test_file_helper.py +++ b/tests/operations/push/test_file_helper.py @@ -17,30 +17,30 @@ def test_unzip(self): self.assertFileUnzipped() def test_unzip_with_existing_output_subdir(self): - os.makedirs(os.path.join(self.output_dirpath, 'some_dir', 'subdir')) + os.makedirs(os.path.join(self.output_dirpath, "some_dir", "subdir")) self.unzip() self.assertFileUnzipped() def unzip(self): - FileHelper.unzip(os.path.join(self.input_dirpath, 'myzip.zip'), self.output_dirpath) + FileHelper.unzip(os.path.join(self.input_dirpath, "myzip.zip"), self.output_dirpath) def zip_some_data(self): input_dirpath = self.prepare_data_to_zip() - with zipfile.ZipFile(os.path.join(input_dirpath, 'myzip.zip'), 'w', zipfile.ZIP_DEFLATED) as myzip: - myzip.write(os.path.join(input_dirpath, 'file.txt'), 'file.txt') - myzip.write(os.path.join(input_dirpath, 'some_dir'), 'some_dir') - myzip.write(os.path.join(input_dirpath, 'some_dir', 'subdir'), 'some_dir/subdir') + with zipfile.ZipFile(os.path.join(input_dirpath, "myzip.zip"), "w", zipfile.ZIP_DEFLATED) as myzip: + myzip.write(os.path.join(input_dirpath, "file.txt"), "file.txt") + myzip.write(os.path.join(input_dirpath, "some_dir"), "some_dir") + myzip.write(os.path.join(input_dirpath, "some_dir", "subdir"), "some_dir/subdir") return input_dirpath def prepare_data_to_zip(self): input_dirpath = tempfile.mkdtemp() - shutil.copyfile(__file__, os.path.join(input_dirpath, 'file.txt')) - os.makedirs(os.path.join(input_dirpath, 'some_dir', 'subdir')) - shutil.copyfile(__file__, os.path.join(input_dirpath, 'some_dir', 'file.txt')) + shutil.copyfile(__file__, os.path.join(input_dirpath, "file.txt")) + os.makedirs(os.path.join(input_dirpath, "some_dir", "subdir")) + shutil.copyfile(__file__, os.path.join(input_dirpath, "some_dir", "file.txt")) return input_dirpath def assertFileUnzipped(self): - self.assertTrue(os.path.isfile(os.path.join(self.output_dirpath, 'file.txt'))) - self.assertTrue(os.path.isdir(os.path.join(self.output_dirpath, 'some_dir'))) - self.assertTrue(os.path.isdir(os.path.join(self.output_dirpath, 'some_dir', 'subdir'))) + self.assertTrue(os.path.isfile(os.path.join(self.output_dirpath, "file.txt"))) + self.assertTrue(os.path.isdir(os.path.join(self.output_dirpath, "some_dir"))) + self.assertTrue(os.path.isdir(os.path.join(self.output_dirpath, "some_dir", "subdir"))) diff --git a/tests/operations/push/test_push.py b/tests/operations/push/test_push.py new file mode 100644 index 0000000..41321b6 --- /dev/null +++ b/tests/operations/push/test_push.py @@ -0,0 +1,68 @@ +import sys +from unittest import TestCase +from unittest.mock import patch, MagicMock + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.operations.push.push import PushOperation + + +class TestPushOperation(TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_split_route_with_port_and_path(self): + domain, port, path = PushOperation._split_route(dict(route="foo-((suffix)).apps.internal:666/some/path")) + self.assertEqual("foo-((suffix)).apps.internal", domain) + self.assertEqual(666, port) + self.assertEqual("/some/path", path) + + def test_split_route_without_port_and_path(self): + domain, port, path = PushOperation._split_route(dict(route="foo-((suffix)).apps.internal")) + self.assertEqual("foo-((suffix)).apps.internal", domain) + self.assertIsNone(port) + self.assertEqual("", path) + + def test_split_route_without_port_path(self): + domain, port, path = PushOperation._split_route(dict(route="foo-((suffix)).apps.internal/path")) + self.assertEqual("foo-((suffix)).apps.internal", domain) + self.assertIsNone(port) + self.assertEqual("/path", path) + + def test_split_route_without_path(self): + domain, port, path = PushOperation._split_route(dict(route="foo-((suffix)).apps.internal:666")) + self.assertEqual("foo-((suffix)).apps.internal", domain) + self.assertEqual(666, port) + self.assertEqual("", path) + + def test_to_host_should_remove_unwanted_characters(self): + host = PushOperation._to_host("idzone-3.0.7-rec-tb1_bobby") + self.assertEqual("idzone-307-rec-tb1-bobby", host) + + @patch.object( + sys, + "argv", + [ + "main", + "push_app", + AbstractTestCase.get_fixtures_path("fake", "operations", "manifest_main.yml"), + "-space_guid", + "space_id" + ], + ) + def test_main_push(self): + class FakeOperation(object): + def __init__(self): + self.push = MagicMock() + + client = object() + push_operation = FakeOperation() + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: client), patch( + "cloudfoundry_client.main.operation_commands.PushOperation", new=lambda c: push_operation + ): + main.main() + push_operation.push.assert_called_with("space_id", self.get_fixtures_path("fake", "operations", "manifest_main.yml")) diff --git a/tests/operations/push/validation/__init__.py b/tests/operations/push/validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/operations/push/validation/test_manifest_reader.py b/tests/operations/push/validation/test_manifest_reader.py new file mode 100644 index 0000000..0182cf2 --- /dev/null +++ b/tests/operations/push/validation/test_manifest_reader.py @@ -0,0 +1,125 @@ +import os +import unittest + +from cloudfoundry_client.operations.push.validation.manifest import ManifestReader + + +class TestManifestReader(unittest.TestCase): + def test_empty_manifest_should_raise_exception(self): + manifest_file = os.path.join(os.path.dirname(__file__), "..", "..", "..", "fixtures", "operations", "manifest_empty.yml") + self.assertRaises(AssertionError, lambda: ManifestReader.load_application_manifests(manifest_file)) + + def test_manifest_should_be_read(self): + manifest_file = os.path.join(os.path.dirname(__file__), "..", "..", "..", "fixtures", "operations", "manifest.yml") + applications = ManifestReader.load_application_manifests(manifest_file) + self.assertEqual(1, len(applications)) + self.assertEqual( + dict( + docker=dict(username="the-user", password="P@SsW0r$", image="some-image"), + name="the-name", + routes=[dict(route="first-route"), dict(route="second-route")], + ), + applications[0], + ) + + def test_complex_manifest_should_be_read(self): + manifest_file = os.path.join( + os.path.dirname(__file__), "..", "..", "..", "fixtures", "operations", "manifest_complex.yml" + ) + applications = ManifestReader.load_application_manifests(manifest_file) + self.assertEqual(2, len(applications)) + self.assertEqual( + dict( + name="bigapp", + buildpacks=["staticfile_buildpack"], + memory=1024, + path=os.path.abspath(os.path.join(os.path.dirname(manifest_file), "big")), + ), + applications[0], + ) + self.assertEqual( + dict( + name="smallapp", + buildpacks=["staticfile_buildpack"], + memory=256, + path=os.path.abspath(os.path.join(os.path.dirname(manifest_file), "small")), + ), + applications[1], + ) + + def test_name_should_be_set(self): + manifest = dict(path="test/") + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_application_should_declare_either_path_or_docker(self): + manifest = dict(name="the-name", docker=dict(), path="test/") + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_application_should_declare_at_least_path_or_docker(self): + manifest = dict(name="the-name", routes=[], environment=dict()) + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_deprecated_entries_should_not_be_set(self): + for deprecated in ["host", "hosts", "domain", "domains", "no-hostname"]: + manifest = dict(name="the-name", path="test/") + manifest[deprecated] = "some-value" + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_docker_manifest_should_declare_buildpack_or_image(self): + manifest = dict(name="the-name", docker=dict(image="some-image", buildpack="some-buildpack")) + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_username_should_be_set_if_password_is(self): + manifest = dict(name="the-name", docker=dict(image="some-image", password="P@SsW0r$")) + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_password_should_be_set_if_username_is(self): + manifest = dict(name="the-name", docker=dict(image="some-image", username="the-user")) + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_username_and_password_are_set_when_image_is(self): + manifest = dict(name="the-name", docker=dict(username="the-user", password="P@SsW0r$")) + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_routes_should_be_an_object_with_attribute(self): + manifest = dict(name="the-name", path="test/", routes=["a route"]) + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + manifest = dict(name="the-name", path="test/", routes=[dict(invalid_attribute="any-value")]) + self.assertRaises(AssertionError, lambda: ManifestReader._validate_application_manifest(".", manifest)) + + def test_valid_application_with_path_and_routes(self): + manifest = dict(name="the-name", path="test/", routes=[dict(route="first-route"), dict(route="second-route")]) + ManifestReader._validate_application_manifest(".", manifest) + + def test_valid_application_with_docker_and_routes(self): + manifest = dict( + docker=dict(username="the-user", password="P@SsW0r$", image="some-image"), + name="the-name", + routes=[dict(route="first-route"), dict(route="second-route")], + ) + ManifestReader._validate_application_manifest(".", manifest) + + def path_should_be_set_as_absolute(self): + manifest = dict(name="the-name", path="test/") + ManifestReader._validate_application_manifest(".", manifest) + self.assertEqual(os.path.abspath("test"), manifest["path"]) + + def test_memory_in_mb(self): + manifest = dict(memory="2048MB") + ManifestReader._convert_size_fields(manifest) + self.assertEqual(2048, manifest["memory"]) + + def test_memory_in_gb(self): + manifest = dict(memory="1G") + ManifestReader._convert_size_fields(manifest) + self.assertEqual(1024, manifest["memory"]) + + def test_disk_quota_in_mb(self): + manifest = dict(disk_quota="2048MB") + ManifestReader._convert_size_fields(manifest) + self.assertEqual(2048, manifest["disk_quota"]) + + def test_disk_quota_in_gb(self): + manifest = dict(disk_quota="1G") + ManifestReader._convert_size_fields(manifest) + self.assertEqual(1024, manifest["disk_quota"]) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..1437d6e --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,269 @@ +import json +from os import remove as file_remove +import unittest +from http import HTTPStatus +from unittest.mock import patch +from urllib.parse import quote + +from cloudfoundry_client.errors import InvalidStatusCode + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.client import CloudFoundryClient +from fake_requests import MockResponse, MockSession, FakeRequests + + +class TestCloudfoundryClient( + unittest.TestCase, + AbstractTestCase, +): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def test_build_client_when_no_log_stream(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests, with_log_streams=False) + client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque") + self.assertRaises(NotImplementedError, lambda: client.rlpgateway) + + def test_build_client_when_no_doppler(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests, with_doppler=False) + client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque") + self.assertRaises(NotImplementedError, lambda: client.doppler) + + def test_build_client_when_no_v2(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests, with_v2=False) + client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque") + self.assertRaises(NotImplementedError, lambda: client.v2) + + def test_build_client_when_no_v3(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests, with_v3=False) + client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque") + self.assertRaises(NotImplementedError, lambda: client.v3) + + def test_grant_password_request_with_token_format_opaque(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests) + requests.post.return_value = MockResponse( + "%s/oauth/token" % self.AUTHORIZATION_ENDPOINT, + status_code=HTTPStatus.OK.value, + text=json.dumps(dict(access_token="access-token", refresh_token="refresh-token")), + ) + client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque") + client.init_with_user_credentials("somebody", "p@s$w0rd") + self.assertEqual("Bearer access-token", session.headers.get("Authorization")) + requests.post.assert_called_with( + requests.post.return_value.url, + data=dict(grant_type="password", username="somebody", scope="", password="p@s$w0rd", token_format="opaque"), + headers=dict(Accept="application/json", Authorization="Basic Y2Y6"), + proxies=dict(http="", https=""), + verify=True, + ) + + def test_refresh_request_with_token_format_opaque(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests) + requests.post.return_value = MockResponse( + "%s/oauth/token" % self.AUTHORIZATION_ENDPOINT, + status_code=HTTPStatus.OK.value, + text=json.dumps(dict(access_token="access-token", refresh_token="refresh-token")), + ) + client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque") + client.init_with_token("refresh-token") + self.assertEqual("Bearer access-token", session.headers.get("Authorization")) + requests.post.assert_called_with( + requests.post.return_value.url, + data=dict(grant_type="refresh_token", scope="", refresh_token="refresh-token", token_format="opaque"), + headers=dict(Accept="application/json", Authorization="Basic Y2Y6"), + proxies=dict(http="", https=""), + verify=True, + ) + + def test_refresh_request_with_token_from_cf_config(self): + requests = FakeRequests() + session = MockSession() + proxy = dict(http='', https='') + cf_config_file_content = {"Target": self.TARGET_ENDPOINT, "RefreshToken": "refresh-token"} + with open("config.json", "w", encoding="utf-8") as f: + json.dump(cf_config_file_content, f, ensure_ascii=False, indent=4) + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests) + requests.post.return_value = MockResponse( + "%s/oauth/token" % self.AUTHORIZATION_ENDPOINT, + status_code=HTTPStatus.OK.value, + text=json.dumps(dict(access_token="access-token", refresh_token="refresh-token")), + ) + client = CloudFoundryClient.build_from_cf_config("config.json", proxy=proxy, verify=True) # noqa: F841 + file_remove('config.json') + self.assertEqual("Bearer access-token", session.headers.get("Authorization")) + requests.post.assert_called_with( + requests.post.return_value.url, + data=dict(grant_type="refresh_token", scope="", refresh_token="refresh-token"), + headers=dict(Accept="application/json", Authorization="Basic Y2Y6"), + proxies=proxy, + verify=True, + ) + + def test_grant_password_request_with_login_hint(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests) + requests.post.return_value = MockResponse( + "%s/oauth/token" % self.AUTHORIZATION_ENDPOINT, + status_code=HTTPStatus.OK.value, + text=json.dumps(dict(access_token="access-token", refresh_token="refresh-token")), + ) + client = CloudFoundryClient( + self.TARGET_ENDPOINT, login_hint=quote(json.dumps(dict(origin="uaa"), separators=(",", ":"))) + ) + client.init_with_user_credentials("somebody", "p@s$w0rd") + self.assertEqual("Bearer access-token", session.headers.get("Authorization")) + requests.post.assert_called_with( + requests.post.return_value.url, + data=dict( + grant_type="password", + username="somebody", + scope="", + password="p@s$w0rd", + login_hint="%7B%22origin%22%3A%22uaa%22%7D", + ), + headers=dict(Accept="application/json", Authorization="Basic Y2Y6"), + proxies=dict(http="", https=""), + verify=True, + ) + + def test_get_info(self): + requests = FakeRequests() + session = MockSession() + with patch("oauth2_client.credentials_manager.requests", new=requests), patch( + "cloudfoundry_client.client.requests", new=requests + ): + requests.Session.return_value = session + self._mock_info_calls(requests) + client = CloudFoundryClient(self.TARGET_ENDPOINT) + self._mock_info_calls(requests) + info = client._get_info(self.TARGET_ENDPOINT) + self.assertEqual(info.api_endpoint, self.TARGET_ENDPOINT) + self.assertEqual(info.api_v2_url, "%s/v2" % self.TARGET_ENDPOINT) + self.assertEqual(info.api_v3_url, "%s/v3" % self.TARGET_ENDPOINT) + self.assertEqual(info.doppler_endpoint, self.DOPPLER_ENDPOINT) + self.assertEqual(info.log_stream_endpoint, self.LOG_STREAM_ENDPOINT) + + def test_invalid_token_v3(self): + response = MockResponse( + "http://some-cf-url", + 401, + text=json.dumps( + dict( + errors=[ + dict(code=666, title="Some-Error", detail="Error detail"), + dict(code=1000, title="CF-InvalidAuthToken", detail="Invalid token"), + ] + ) + ), + ) + result = CloudFoundryClient._is_token_expired(response) + self.assertTrue(result) + + def test_invalid_token_v2(self): + response = MockResponse("http://some-cf-url", 401, text=json.dumps(dict(code=1000, error_code="CF-InvalidAuthToken"))) + result = CloudFoundryClient._is_token_expired(response) + self.assertTrue(result) + + def test_log_request(self): + response = MockResponse( + "http://some-cf-url", + 200, + text=json.dumps(dict(entity="entityTest", metadata="metadataTest")), + headers={"x-vcap-request-id": "testVcap"}, + ) + with self.assertLogs(level="DEBUG") as cm: + CloudFoundryClient._log_request("GET", "testURL", response) + self.assertEqual( + cm.output, + [ + "DEBUG:cloudfoundry_client.client:GET: url=testURL - status_code=200 - vcap-request-id=testVcap - response=" + '{"entity": "entityTest", "metadata": "metadataTest"}' + ], + ) + + def test_log_request_empty_headers(self): + response = MockResponse("http://some-cf-url", 200, text=json.dumps(dict(entity="entityTest", metadata="metadataTest"))) + with self.assertLogs(level="DEBUG") as cm: + CloudFoundryClient._log_request("GET", "testURL", response) + self.assertEqual( + cm.output, + [ + "DEBUG:cloudfoundry_client.client:GET: url=testURL - status_code=200 - vcap-request-id=N/A - response=" + '{"entity": "entityTest", "metadata": "metadataTest"}' + ], + ) + + def test_check_response_500_without_vcap(self): + response = MockResponse("http://some-cf-url", 500, text=json.dumps(dict(entity="entityTest", metadata="metadataTest"))) + with self.assertRaises(InvalidStatusCode): + CloudFoundryClient._check_response(response) + + def test_check_response_500_with_vcap(self): + response = MockResponse( + "http://some-cf-url", + 500, + text=json.dumps(dict(entity="entityTest", metadata="metadataTest")), + headers={"x-vcap-request-id": "testVcap"}, + ) + with self.assertRaisesRegex(InvalidStatusCode, "testVcap"): + CloudFoundryClient._check_response(response) + + def test_check_response_500_text(self): + response = MockResponse("http://some-cf-url", 500, text="This is test text") + with self.assertRaisesRegex(InvalidStatusCode, "This is test text"): + CloudFoundryClient._check_response(response) + + def test_check_response_500_json(self): + response = MockResponse("http://some-cf-url", 500, text=json.dumps(dict(entity="entityTest", metadata="metadataTest"))) + with self.assertRaisesRegex(InvalidStatusCode, "metadataTest"): + CloudFoundryClient._check_response(response) + + def test_check_response_200(self): + response = MockResponse("http://some-cf-url", 200, text=json.dumps(dict(entity="entityTest", metadata="metadataTest"))) + self.assertIsNotNone(CloudFoundryClient._check_response(response)) diff --git a/tests/test_doppler.py b/tests/test_doppler.py new file mode 100644 index 0000000..02ec451 --- /dev/null +++ b/tests/test_doppler.py @@ -0,0 +1,33 @@ +import re +import unittest +from functools import reduce +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.doppler.client import DopplerClient + + +class TestLoggregator(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + self.doppler = DopplerClient( + re.sub("^http", "ws", self.TARGET_ENDPOINT), proxy="", verify_ssl=False, credentials_manager=self.client + ) + + def test_recents(self): + boundary = "d661b2c1426a3abcf1c0524d7fdbc774c42a767bdd6702141702d16047bc" + app_guid = "app_id" + self.client.get.return_value = self.mock_response( + "/apps/%s/recentlogs" % app_guid, + HTTPStatus.OK, + {"content-type": "multipart/x-protobuf; boundary=%s" % boundary}, + "recents", + "GET_response.bin", + ) + cpt = reduce(lambda increment, _: increment + 1, self.doppler.recent_logs(app_guid), 0) + self.client.get.assert_called_with(self.client.get.return_value.url, stream=True) + self.assertEqual(cpt, 200) diff --git a/tests/test_request_object.py b/tests/test_request_object.py new file mode 100644 index 0000000..1c24deb --- /dev/null +++ b/tests/test_request_object.py @@ -0,0 +1,16 @@ +import unittest + +from cloudfoundry_client.common_objects import Request + + +class TestRequest(unittest.TestCase): + def test_mandatory_should_be_present_even_when_none(self): + request = Request(mandatory=None) + self.assertTrue("mandatory" in request) + self.assertIsNone(request["mandatory"]) + + def test_optional_should_not_be_present_when_none(self): + request = Request(mandatory="value") + request["optional"] = None + self.assertEqual("value", request["mandatory"]) + self.assertTrue("optional" not in request) diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/v2/test_apps.py b/tests/v2/test_apps.py new file mode 100644 index 0000000..59cdce3 --- /dev/null +++ b/tests/v2/test_apps.py @@ -0,0 +1,275 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call, patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.errors import InvalidStatusCode + + +class TestApps(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response("/v2/apps", HTTPStatus.OK, None, "v2", "apps", "GET_response.json") + all_applications = [application for application in self.client.v2.apps.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(len(all_applications), 3) + print("test_list - Application - %s" % str(all_applications[0])) + self.assertEqual(all_applications[0]["entity"]["name"], "name-423") + + def test_list_filtered(self): + self.client.get.return_value = self.mock_response( + "/v2/apps?q=name%3Aapplication_name&results-per-page=1&q=space_guid%3Aspace_guid", + HTTPStatus.OK, + None, + "v2", + "apps", + "GET_space_guid_name_response.json", + ) + application = self.client.v2.apps.get_first(space_guid="space_guid", name="application_name") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(application) + + def test_get_env(self): + self.client.get.return_value = self.mock_response( + "/v2/apps/app_id/env", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_env_response.json" + ) + application = self.client.v2.apps.get_env("app_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(application) + + def test_get_instances(self): + self.client.get.return_value = self.mock_response( + "/v2/apps/app_id/instances", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_instances_response.json" + ) + application = self.client.v2.apps.get_instances("app_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(application) + + def test_get_stats(self): + self.client.get.return_value = self.mock_response( + "/v2/apps/app_id/stats", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_stats_response.json" + ) + application = self.client.v2.apps.get_stats("app_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(application) + + def test_associate_route(self): + self.client.put.return_value = self.mock_response( + "/v2/apps/app_id/routes/route_id", HTTPStatus.CREATED, None, "v2", "apps", "PUT_{id}_routes_{route_id}_response.json" + ) + self.client.v2.apps.associate_route("app_id", "route_id") + self.client.put.assert_called_with(self.client.put.return_value.url, json=None) + + def test_list_routes(self): + self.client.get.return_value = self.mock_response( + "/v2/apps/app_id/routes?q=route_guid%3Aroute_id", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_routes_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.apps.list_routes("app_id", route_guid="route_id"), 0) + for route in self.client.v2.apps.list_routes("app_id", route_guid="route_id"): + print(route) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_remove_route(self): + self.client.delete.return_value = self.mock_response("/v2/apps/app_id/routes/route_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.apps.remove_route("app_id", "route_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_list_service_bindings(self): + self.client.get.return_value = self.mock_response( + "/v2/apps/app_id/service_bindings", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_service_bindings_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.apps.list_service_bindings("app_id"), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get_sumary(self): + self.client.get.return_value = self.mock_response( + "/v2/apps/app_id/summary", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_summary_response.json" + ) + application = self.client.v2.apps.get_summary("app_id") + + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(application) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/apps/app_id", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_response.json" + ) + application = self.client.v2.apps.get("app_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(application) + + def test_start(self): + self.client.put.return_value = self.mock_response( + "/v2/apps/app_id", HTTPStatus.CREATED, None, "v2", "apps", "PUT_{id}_response.json" + ) + mock_summary = self.mock_response( + "/v2/apps/app_id/summary", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_summary_response.json" + ) + mock_instances_stopped = self.mock_response( + "/v2/apps/app_id/instances", HTTPStatus.BAD_REQUEST, None, "v2", "apps", "GET_{id}_instances_stopped_response.json" + ) + mock_instances_started = self.mock_response( + "/v2/apps/app_id/instances", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_instances_response.json" + ) + self.client.get.side_effect = [ + mock_summary, + InvalidStatusCode(HTTPStatus.BAD_REQUEST, dict(code=220001)), + mock_instances_started, + ] + + application = self.client.v2.apps.start("app_id") + self.client.put.assert_called_with(self.client.put.return_value.url, json=dict(state="STARTED")) + self.client.get.assert_has_calls( + [call(mock_summary.url), call(mock_instances_stopped.url), call(mock_instances_started.url)], any_order=False + ) + self.assertIsNotNone(application) + + def test_stop(self): + self.client.put.return_value = self.mock_response( + "/v2/apps/app_id", HTTPStatus.CREATED, None, "v2", "apps", "PUT_{id}_response.json" + ) + self.client.get.side_effect = [InvalidStatusCode(HTTPStatus.BAD_REQUEST, dict(code=220001))] + application = self.client.v2.apps.stop("app_id") + self.client.put.assert_called_with(self.client.put.return_value.url, json=dict(state="STOPPED")) + self.client.get.assert_called_with("%s/v2/apps/app_id/instances" % self.TARGET_ENDPOINT) + self.assertIsNotNone(application) + + def test_restart_instance(self): + self.client.delete.return_value = self.mock_response("/v2/apps/app_id/instances/666", HTTPStatus.NO_CONTENT, None) + self.client.v2.apps.restart_instance("app_id", 666) + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v2/apps", HTTPStatus.CREATED, None, "v2", "apps", "POST_response.json" + ) + application = self.client.v2.apps.create( + name="test", + space_guid="1fbb3e81-4f55-4fd3-9820-45febbd5e53e", + stack_guid="82f9c01c-72f2-4d3e-b5ed-eab97a6203cf", + memory=1024, + instances=1, + disk_quota=1024, + health_check_type="port", + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json=dict( + name="test", + space_guid="1fbb3e81-4f55-4fd3-9820-45febbd5e53e", + stack_guid="82f9c01c-72f2-4d3e-b5ed-eab97a6203cf", + memory=1024, + instances=1, + disk_quota=1024, + health_check_type="port", + ), + ) + self.assertIsNotNone(application) + + def test_update(self): + self.client.put.return_value = self.mock_response( + "/v2/apps/app_id", HTTPStatus.CREATED, None, "v2", "apps", "PUT_{id}_response.json" + ) + application = self.client.v2.apps.update( + "app_id", + stack_guid="82f9c01c-72f2-4d3e-b5ed-eab97a6203cf", + memory=1024, + instances=1, + disk_quota=1024, + health_check_type="port", + ) + self.client.put.assert_called_with( + self.client.put.return_value.url, + json=dict( + stack_guid="82f9c01c-72f2-4d3e-b5ed-eab97a6203cf", + memory=1024, + instances=1, + disk_quota=1024, + health_check_type="port", + ), + ) + self.assertIsNotNone(application) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v2/apps/app_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.apps.remove("app_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_restage(self): + self.client.post.return_value = self.mock_response( + "/v2/apps/app_id/restage", HTTPStatus.CREATED, None, "v2", "apps", "POST_{id}_restage_response.json" + ) + self.client.v2.apps.restage("app_id") + self.client.post.assert_called_with(self.client.post.return_value.url, json=None) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response("/v2/apps/app_id", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_response.json"), + self.mock_response("/v2/spaces/space_id", HTTPStatus.OK, None, "v2", "spaces", "GET_{id}_response.json"), + self.mock_response("/v2/routes", HTTPStatus.OK, None, "v2", "routes", "GET_response.json"), + ] + application = self.client.v2.apps.get("app_id") + + self.assertIsNotNone(application.space()) + cpt = reduce(lambda increment, _: increment + 1, application.routes(), 0) + self.assertEqual(cpt, 1) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) + + @patch.object(sys, "argv", ["main", "list_apps"]) + def test_main_list_apps(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response("/v2/apps", HTTPStatus.OK, None, "v2", "apps", "GET_response.json") + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "delete_app", "906775ea-622e-4bc7-af5d-9aab3b652f81"]) + def test_main_delete_apps(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.delete.return_value = self.mock_response( + "/v2/apps/906775ea-622e-4bc7-af5d-9aab3b652f81", HTTPStatus.NO_CONTENT, None + ) + main.main() + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object(sys, "argv", ["main", "get_app", "906775ea-622e-4bc7-af5d-9aab3b652f81"]) + def test_main_get_app(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/apps/906775ea-622e-4bc7-af5d-9aab3b652f81", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "restage", "906775ea-622e-4bc7-af5d-9aab3b652f81"]) + def test_main_restage_app(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.post.return_value = self.mock_response( + "/v2/apps/906775ea-622e-4bc7-af5d-9aab3b652f81/restage", + HTTPStatus.CREATED, + None, + "v2", + "apps", + "POST_{id}_restage_response.json", + ) + main.main() + self.client.post.assert_called_with(self.client.post.return_value.url, json=None) + + @patch.object(sys, "argv", ["main", "restart_instance", "906775ea-622e-4bc7-af5d-9aab3b652f81", "666"]) + def test_main_restart_app_instance(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.delete.return_value = self.mock_response( + "/v2/apps/906775ea-622e-4bc7-af5d-9aab3b652f81/instances/666", HTTPStatus.NO_CONTENT, None + ) + main.main() + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v2/test_buildpacks.py b/tests/v2/test_buildpacks.py new file mode 100644 index 0000000..e37b6d4 --- /dev/null +++ b/tests/v2/test_buildpacks.py @@ -0,0 +1,38 @@ +import unittest +from functools import reduce +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase + + +class TestBuildpacks(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/buildpacks", HTTPStatus.OK, None, "v2", "buildpacks", "GET_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.buildpacks.list(), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 3) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/buildpacks/buildpack_id", HTTPStatus.OK, None, "v2", "buildpacks", "GET_{id}_response.json" + ) + result = self.client.v2.buildpacks.get("buildpack_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_update(self): + self.client.put.return_value = self.mock_response( + "/v2/buildpacks/build_pack_id", HTTPStatus.CREATED, None, "v2", "apps", "PUT_{id}_response.json" + ) + result = self.client.v2.buildpacks.update("build_pack_id", dict(enabled=False)) + self.client.put.assert_called_with(self.client.put.return_value.url, json=dict(enabled=False)) + self.assertIsNotNone(result) diff --git a/tests/v2/test_entities.py b/tests/v2/test_entities.py new file mode 100644 index 0000000..d88920e --- /dev/null +++ b/tests/v2/test_entities.py @@ -0,0 +1,187 @@ +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import MagicMock, call + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.errors import InvalidEntity +from cloudfoundry_client.v2.entities import EntityManager, Entity + + +class TestEntities(unittest.TestCase, AbstractTestCase): + def test_invalid_entity_without_entity_attribute(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/anyone") + + client.get.return_value = self.mock_response( + "/fake/anyone/any-id", HTTPStatus.OK, None, "v2", "fake", "GET_invalid_entity_without_entity.json" + ) + + self.assertRaises(InvalidEntity, lambda: entity_manager["any-id"]) + + def test_invalid_entity_with_null_entity(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/anyone") + + client.get.return_value = self.mock_response( + "/fake/anyone/any-id", HTTPStatus.OK, None, "v2", "fake", "GET_invalid_entity_with_null_entity.json" + ) + + self.assertRaises(InvalidEntity, lambda: entity_manager["any-id"]) + + def test_invalid_entity_with_invalid_entity_type(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/anyone") + + client.get.return_value = self.mock_response( + "/fake/anyone/any-id", HTTPStatus.OK, None, "v2", "fake", "GET_invalid_entity_with_invalid_entity_type.json" + ) + + self.assertRaises(InvalidEntity, lambda: entity_manager["any-id"]) + + def test_query(self): + url = EntityManager("http://cf.api", None, "/v2/apps")._get_url_filtered( + "/v2/apps", **{"results-per-page": 20, "order-direction": "asc", "page": 1, "space_guid": "id", "order-by": "id"} + ) + self.assertEqual("/v2/apps?order-by=id&order-direction=asc&page=1&results-per-page=20&q=space_guid%3Aid", url) + + def test_query_multi_order_by(self): + url = EntityManager("http://cf.api", None, "/v2/apps")._get_url_filtered("/v2/apps", **{"order-by": ["timestamp", "id"]}) + self.assertEqual("/v2/apps?order-by=timestamp&order-by=id", url) + + def test_query_single_order_by(self): + url = EntityManager("http://cf.api", None, "/v2/apps")._get_url_filtered("/v2/apps", **{"order-by": "timestamp"}) + self.assertEqual("/v2/apps?order-by=timestamp", url) + + def test_query_in(self): + url = EntityManager("http://cf.api", None, "/v2/apps")._get_url_filtered( + "/v2/apps", **{"results-per-page": 20, "order-direction": "asc", "page": 1, "space_guid": ["id1", "id2"]} + ) + self.assertEqual("/v2/apps?order-direction=asc&page=1&results-per-page=20&q=space_guid%20IN%20id1%2Cid2", url) + + def test_multi_query(self): + url = EntityManager("http://cf.api", None, "/v2/events")._get_url_filtered( + "/v2/events", **{"type": ["create", "update"], "organization_guid": "org-id"} + ) + self.assertEqual("/v2/events?q=organization_guid%3Aorg-id&q=type%20IN%20create%2Cupdate", url) + + def test_range_query(self): + url = EntityManager("http://cf.api", None, "/v2/events")._get_url_filtered( + "/v2/events", **{"type": "app.crash", "space_guid": "space-id", "timestamp": {">": "2022-02-08T16:41:25Z"}} + ) + self.assertEqual("/v2/events?q=space_guid%3Aspace-id&q=timestamp%3E2022-02-08T16%3A41%3A25Z&q=type%3Aapp.crash", url) + + def test_list(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/first") + + first_response = self.mock_response( + "/fake/first?order-direction=asc&page=1&results-per-page=20&q=space_guid%3Asome-id", + HTTPStatus.OK, + None, + "v2", "fake", + "GET_multi_page_0_response.json", + ) + second_response = self.mock_response( + "/fake/next?order-direction=asc&page=2&results-per-page=50", + HTTPStatus.OK, + None, + "v2", "fake", + "GET_multi_page_1_response.json", + ) + + client.get.side_effect = [first_response, second_response] + guids = reduce( + lambda c, entity: c.append(entity["metadata"]["guid"]) or c, + entity_manager.list(**{"results-per-page": 20, "order-direction": "asc", "page": 1, "space_guid": "some-id"}), + [], + ) + client.get.assert_has_calls([call(first_response.url), call(second_response.url)], any_order=False) + self.assertEqual(guids, [ + "6fa7a340-9bda-43bf-bd5e-4e588c292679", + "7002efa8-3f54-4338-8884-117e98f21566", + "774a9f7e-895d-4825-84fc-222c1522a9a7" + ]) + + def test_elements_are_entities(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/first") + + first_response = self.mock_response( + "/fake/first?order-direction=asc&page=1&results-per-page=20&q=space_guid%3Asome-id", + HTTPStatus.OK, + None, + "v2", "fake", + "GET_multi_page_0_response.json", + ) + second_response = self.mock_response( + "/fake/next?order-direction=asc&page=2&results-per-page=50", + HTTPStatus.OK, + None, + "v2", "fake", + "GET_multi_page_1_response.json", + ) + client.get.side_effect = [first_response, second_response] + + entities_list = entity_manager.list( + **{"results-per-page": 20, "order-direction": "asc", "page": 1, "space_guid": "some-id"}) + + for entity in entities_list: + self.assertIsInstance(entity, Entity) + + def test_iter(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + + client.get.return_value = self.mock_response("/fake/something", HTTPStatus.OK, None, "v2", "fake", "GET_response.json") + cpt = reduce(lambda increment, _: increment + 1, entity_manager, 0) + client.get.assert_called_with(client.get.return_value.url) + + self.assertEqual(cpt, 2) + + def test_get_elem(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + + client.get.return_value = self.mock_response( + "/fake/something/with-id", HTTPStatus.OK, None, "v2", "fake", "GET_{id}_response.json" + ) + entity = entity_manager["with-id"] + client.get.assert_called_with(client.get.return_value.url) + + self.assertEqual(entity["entity"]["name"], "name-423") + + def test_entity_manager_is_a_generator(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + client.get.return_value = self.mock_response( + "/fake/something/with-id", HTTPStatus.OK, None, "v2", "fake", "GET_{id}_response.json" + ) + + self.assertIsNotNone(getattr(entity_manager, "__iter__", None)) + self.assertIsNotNone(getattr(entity_manager.__iter__, "__call__", None)) + generator = entity_manager.__iter__() + self.assertIsNotNone(getattr(generator, "__next__", None)) + self.assertIsNotNone(getattr(generator.__next__, "__call__", None)) + + def test_entity_list_is_a_generator(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + client.get.return_value = self.mock_response( + "/fake/something/with-id", HTTPStatus.OK, None, "v2", "fake", "GET_{id}_response.json" + ) + + generator = entity_manager.list() + + self.assertIsNotNone(getattr(generator, "__next__", None)) + self.assertIsNotNone(getattr(generator.__next__, "__call__", None)) + + def test_total_results(self): + client = MagicMock() + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + client.get.return_value = self.mock_response("/fake/something", HTTPStatus.OK, None, "v2", "fake", "GET_response.json") + + cpt = entity_manager.list().total_results + + self.assertEqual(cpt, 3) + client.get.assert_called_with(client.get.return_value.url) diff --git a/tests/v2/test_events.py b/tests/v2/test_events.py new file mode 100644 index 0000000..e9764fc --- /dev/null +++ b/tests/v2/test_events.py @@ -0,0 +1,28 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase + + +class TestEvents(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/events?q=type%3Aaudit.route.delete-request", + HTTPStatus.OK, + None, + "v2", + "events", + "GET_response_audit.route.delete-request.json", + ) + delete_route_events = [event for event in self.client.v2.event.list_by_type("audit.route.delete-request")] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(len(delete_route_events), 1) + print("test_list - Event - %s" % str(delete_route_events[0])) + self.assertEqual(delete_route_events[0]["entity"]["type"], "audit.route.delete-request") diff --git a/tests/v2/test_organizations.py b/tests/v2/test_organizations.py new file mode 100644 index 0000000..b7f5ed9 --- /dev/null +++ b/tests/v2/test_organizations.py @@ -0,0 +1,48 @@ +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call + +from abstract_test_case import AbstractTestCase + + +class TestOrganizations(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/organizations?q=name%3Aorganization_name", HTTPStatus.OK, None, "v2", "organizations", "GET_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.organizations.list(name="organization_name"), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/organizations/org_id", HTTPStatus.OK, None, "v2", "organizations", "GET_{id}_response.json" + ) + result = self.client.v2.organizations.get("org_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response("/v2/organizations/org_id", HTTPStatus.OK, None, "v2", "organizations", "GET_{id}_response.json"), + self.mock_response( + "/v2/organizations/fe79371b-39b8-4f0d-8331-cff423a06aca/spaces", + HTTPStatus.OK, + None, + "v2", + "spaces", + "GET_response.json", + ), + ] + organization = self.client.v2.organizations.get("org_id") + cpt = reduce(lambda increment, _: increment + 1, organization.spaces(), 0) + self.assertEqual(cpt, 1) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) diff --git a/tests/v2/test_routes.py b/tests/v2/test_routes.py new file mode 100644 index 0000000..040df21 --- /dev/null +++ b/tests/v2/test_routes.py @@ -0,0 +1,76 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call, patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestRoutes(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/routes?q=organization_guid%3Aorganization_guid", HTTPStatus.OK, None, "v2", "routes", "GET_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.routes.list(organization_guid="organization_guid"), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/routes/route_id", HTTPStatus.OK, None, "v2", "routes", "GET_{id}_response.json" + ) + result = self.client.v2.routes.get("route_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response("/v2/routes/route_id", HTTPStatus.OK, None, "v2", "routes", "GET_{id}_response.json"), + self.mock_response( + "/v2/service_instances/e3db4ea8-ab0c-4c47-adf8-a70a8e990ee4", + HTTPStatus.OK, + None, + "v2", + "service_instances", + "GET_{id}_response.json", + ), + self.mock_response( + "/v2/spaces/b3f94ab9-1520-478b-a6d6-eb467c179ada", HTTPStatus.OK, None, "v2", "spaces", "GET_{id}_response.json" + ), + self.mock_response( + "/v2/routes/75c16cfe-9b8a-4faf-bb65-02c713c7956f/apps", HTTPStatus.OK, None, "v2", "apps", "GET_response.json" + ), + ] + route = self.client.v2.routes.get("route_id") + self.assertIsNotNone(route.service_instance()) + self.assertIsNotNone(route.space()) + cpt = reduce(lambda increment, _: increment + 1, route.apps(), 0) + self.assertEqual(cpt, 3) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) + + @patch.object(sys, "argv", ["main", "list_routes"]) + def test_main_list_routes(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/routes", HTTPStatus.OK, None, "v2", "routes", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_route", "75c16cfe-9b8a-4faf-bb65-02c713c7956f"]) + def test_main_get_route(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/routes/75c16cfe-9b8a-4faf-bb65-02c713c7956f", HTTPStatus.OK, None, "v2", "routes", "GET_{id}_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v2/test_service_bindings.py b/tests/v2/test_service_bindings.py new file mode 100644 index 0000000..4d3d21f --- /dev/null +++ b/tests/v2/test_service_bindings.py @@ -0,0 +1,108 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call, patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestServiceBindings(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/service_bindings?q=service_instance_guid%3Ainstance_guid", + HTTPStatus.OK, + None, + "v2", + "service_bindings", + "GET_response.json", + ) + cpt = reduce( + lambda increment, _: increment + 1, self.client.v2.service_bindings.list(service_instance_guid="instance_guid"), 0 + ) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/service_bindings/service_binding_id", HTTPStatus.OK, None, "v2", "service_bindings", "GET_{id}_response.json" + ) + result = self.client.v2.service_bindings.get("service_binding_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v2/service_bindings", HTTPStatus.CREATED, None, "v2", "service_bindings", "POST_response.json" + ) + service_binding = self.client.v2.service_bindings.create( + "app_guid", "instance_guid", dict(the_service_broker="wants this object"), "binding_name" + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json=dict( + app_guid="app_guid", + service_instance_guid="instance_guid", + name="binding_name", + parameters=dict(the_service_broker="wants this object"), + ), + ) + self.assertIsNotNone(service_binding) + + def test_delete(self): + self.client.delete.return_value = self.mock_response("/v2/service_bindings/binding_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_bindings.remove("binding_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response( + "/v2/service_bindings/service_binding_id", HTTPStatus.OK, None, "v2", "service_bindings", "GET_{id}_response.json" + ), + self.mock_response( + "/v2/service_instances/ef0bf611-82c6-4603-99fc-3a1a893109d0", + HTTPStatus.OK, + None, + "v2", + "service_instances", + "GET_{id}_response.json", + ), + self.mock_response( + "/v2/apps/c77953c8-6c35-46c7-816e-cf0c42ac2f52", HTTPStatus.OK, None, "v2", "apps", "GET_{id}_response.json" + ), + ] + service_binding = self.client.v2.service_bindings.get("service_binding_id") + self.assertIsNotNone(service_binding.service_instance()) + self.assertIsNotNone(service_binding.app()) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) + + @patch.object(sys, "argv", ["main", "list_service_bindings"]) + def test_main_list_service_bindings(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_bindings", HTTPStatus.OK, None, "v2", "service_bindings", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_service_binding", "eaabd042-8f5c-44a2-9580-1e114c36bdcb"]) + def test_main_get_service_binding(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_bindings/eaabd042-8f5c-44a2-9580-1e114c36bdcb", + HTTPStatus.OK, + None, + "v2", + "service_bindings", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v2/test_service_brokers.py b/tests/v2/test_service_brokers.py new file mode 100644 index 0000000..8402e20 --- /dev/null +++ b/tests/v2/test_service_brokers.py @@ -0,0 +1,85 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestServiceBrokers(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/service_brokers?q=space_guid%3Aspace_guid", HTTPStatus.OK, None, "v2", "service_bindings", "GET_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.service_brokers.list(space_guid="space_guid"), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/service_brokers/broker_id", HTTPStatus.OK, None, "v2", "service_brokers", "GET_{id}_response.json" + ) + result = self.client.v2.service_brokers.get("broker_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v2/service_brokers", HTTPStatus.CREATED, None, "v2", "service_brokers", "POST_response.json" + ) + service_broker = self.client.v2.service_brokers.create("url", "name", "username", "P@sswd1") + self.client.post.assert_called_with( + self.client.post.return_value.url, + json=dict(broker_url="url", name="name", auth_username="username", auth_password="P@sswd1"), + ) + self.assertIsNotNone(service_broker) + + def test_update(self): + self.client.put.return_value = self.mock_response( + "/v2/service_brokers/broker_id", HTTPStatus.OK, None, "v2", "service_brokers", "PUT_{id}_response.json" + ) + service_broker = self.client.v2.service_brokers.update( + "broker_id", broker_url="new-url", auth_username="new-username", auth_password="P@sswd2" + ) + self.client.put.assert_called_with( + self.client.put.return_value.url, + json=dict(broker_url="new-url", auth_username="new-username", auth_password="P@sswd2"), + ) + self.assertIsNotNone(service_broker) + + def test_delete(self): + self.client.delete.return_value = self.mock_response("/v2/service_brokers/broker_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_brokers.remove("broker_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object(sys, "argv", ["main", "list_service_brokers"]) + def test_main_list_service_brokers(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_brokers", HTTPStatus.OK, None, "v2", "service_brokers", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_service_broker", "ade9730c-4ee5-4290-ad37-0b15cecd2ca6"]) + def test_main_get_service_broker(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_brokers/ade9730c-4ee5-4290-ad37-0b15cecd2ca6", + HTTPStatus.OK, + None, + "v2", + "service_brokers", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v2/test_service_instances.py b/tests/v2/test_service_instances.py new file mode 100644 index 0000000..74f309b --- /dev/null +++ b/tests/v2/test_service_instances.py @@ -0,0 +1,196 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call, patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestServiceInstances(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/service_instances?q=service_plan_guid%3Aplan_id&q=space_guid%3Aspace_guid", + HTTPStatus.OK, + None, + "v2", + "service_instances", + "GET_response.json", + ) + cpt = reduce( + lambda increment, _: increment + 1, + self.client.v2.service_instances.list(space_guid="space_guid", service_plan_guid="plan_id"), + 0, + ) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/service_instances/instance_id", HTTPStatus.OK, None, "v2", "service_instances", "GET_{id}_response.json" + ) + result = self.client.v2.service_instances.get("instance_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v2/service_instances", HTTPStatus.CREATED, None, "v2", "service_instances", "POST_response.json" + ) + service_instance = self.client.v2.service_instances.create( + "space_guid", + "name", + "plan_id", + parameters=dict(the_service_broker="wants this object"), + tags=["mongodb"], + accepts_incomplete=True, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json=dict( + name="name", + space_guid="space_guid", + service_plan_guid="plan_id", + parameters=dict(the_service_broker="wants this object"), + tags=["mongodb"], + ), + params=dict(accepts_incomplete="true"), + ) + self.assertIsNotNone(service_instance) + + def test_update(self): + self.client.put.return_value = self.mock_response( + "/v2/service_instances/instance_id", HTTPStatus.OK, None, "v2", "service_instances", "PUT_{id}_response.json" + ) + service_instance = self.client.v2.service_instances.update("instance_id", instance_name="new-name", tags=["other-tag"]) + self.client.put.assert_called_with( + self.client.put.return_value.url, json=dict(name="new-name", tags=["other-tag"]), params=None + ) + self.assertIsNotNone(service_instance) + + def test_update2(self): + self.client.put.return_value = self.mock_response( + "/v2/service_instances/instance_id", HTTPStatus.OK, None, "v2", "service_instances", "PUT_{id}_response.json" + ) + service_instance = self.client.v2.service_instances.update( + "instance_id", instance_name="new-name", tags=["other-tag"], accepts_incomplete=True + ) + self.client.put.assert_called_with( + self.client.put.return_value.url, + json=dict(name="new-name", tags=["other-tag"]), + params={"accepts_incomplete": "true"}, + ) + self.assertIsNotNone(service_instance) + + def test_delete_accepts_incomplete(self): + self.client.delete.return_value = self.mock_response("/v2/service_instances/instance_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_instances.remove("instance_id", accepts_incomplete=True) + self.client.delete.assert_called_with(self.client.delete.return_value.url, params=dict(accepts_incomplete="true")) + + self.client.delete.return_value = self.mock_response("/v2/service_instances/instance_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_instances.remove("instance_id", accepts_incomplete="true") + self.client.delete.assert_called_with(self.client.delete.return_value.url, params=dict(accepts_incomplete="true")) + + def test_delete_purge(self): + self.client.delete.return_value = self.mock_response("/v2/service_instances/instance_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_instances.remove("instance_id", accepts_incomplete=True, purge=True) + self.client.delete.assert_called_with( + self.client.delete.return_value.url, params=dict(accepts_incomplete="true", purge="true") + ) + + self.client.delete.return_value = self.mock_response("/v2/service_instances/instance_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_instances.remove("instance_id", purge="true") + self.client.delete.assert_called_with(self.client.delete.return_value.url, params=dict(purge="true")) + + self.client.delete.return_value = self.mock_response("/v2/service_instances/instance_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_instances.remove("instance_id", purge="true") + self.client.delete.assert_called_with(self.client.delete.return_value.url, params=dict(purge="true")) + + def test_delete(self): + self.client.delete.return_value = self.mock_response("/v2/service_instances/instance_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_instances.remove("instance_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url, params={}) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response( + "/v2/service_instances/instance_id", HTTPStatus.OK, None, "v2", "service_instances", "GET_{id}_response.json" + ), + self.mock_response( + "/v2/spaces/e3138257-8035-4c03-8aba-ab5d35eec0f9", HTTPStatus.OK, None, "v2", "spaces", "GET_{id}_response.json" + ), + self.mock_response( + "/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96/service_bindings", + HTTPStatus.OK, + None, + "v2", + "service_bindings", + "GET_response.json", + ), + self.mock_response( + "/v2/service_plans/65740f84-214a-46cf-b8e3-2233d580f293", + HTTPStatus.OK, + None, + "v2", + "service_plans", + "GET_{id}_response.json", + ), + self.mock_response( + "/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96/routes", + HTTPStatus.OK, + None, + "v2", + "routes", + "GET_response.json", + ), + self.mock_response( + "/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96/service_keys", + HTTPStatus.OK, + None, + "v2", + "service_keys", + "GET_response.json", + ), + ] + service_instance = self.client.v2.service_instances.get("instance_id") + + self.assertIsNotNone(service_instance.space()) + cpt = reduce(lambda increment, _: increment + 1, service_instance.service_bindings(), 0) + self.assertEqual(cpt, 1) + self.assertIsNotNone(service_instance.service_plan()) + cpt = reduce(lambda increment, _: increment + 1, service_instance.routes(), 0) + self.assertEqual(cpt, 1) + cpt = reduce(lambda increment, _: increment + 1, service_instance.service_keys(), 0) + self.assertEqual(cpt, 1) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) + + @patch.object(sys, "argv", ["main", "list_service_instances"]) + def test_main_list_service_instances(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_instances", HTTPStatus.OK, None, "v2", "service_instances", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_service_instance", "df52420f-d5b9-4b86-a7d3-6d7005d1ce96"]) + def test_main_get_service_instance(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_instances/df52420f-d5b9-4b86-a7d3-6d7005d1ce96", + HTTPStatus.OK, + None, + "v2", + "service_instances", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v2/test_service_keys.py b/tests/v2/test_service_keys.py new file mode 100644 index 0000000..2a6dd8e --- /dev/null +++ b/tests/v2/test_service_keys.py @@ -0,0 +1,104 @@ +import json +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestServiceKeys(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/service_keys?q=service_instance_guid%3Ainstance_guid", + HTTPStatus.OK, + None, + "v2", + "service_keys", + "GET_response.json", + ) + cpt = reduce( + lambda increment, _: increment + 1, self.client.v2.service_keys.list(service_instance_guid="instance_guid"), 0 + ) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/service_keys/key_id", HTTPStatus.OK, None, "v2", "service_keys", "GET_{id}_response.json" + ) + result = self.client.v2.service_keys.get("key_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v2/service_keys", HTTPStatus.CREATED, None, "v2", "service_keys", "POST_response.json" + ) + service_key = self.client.v2.service_keys.create("service_instance_id", "name-127") + self.client.post.assert_called_with( + self.client.post.return_value.url, json=dict(service_instance_guid="service_instance_id", name="name-127") + ) + self.assertIsNotNone(service_key) + + def test_delete(self): + self.client.delete.return_value = self.mock_response("/v2/service_keys/key_id", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_keys.remove("key_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object(sys, "argv", ["main", "list_service_keys"]) + def test_main_list_service_keys(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_keys", HTTPStatus.OK, None, "v2", "service_keys", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_service_key", "67755c27-28ed-4087-9688-c07d92f3bcc9"]) + def test_main_get_service_key(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_keys/67755c27-28ed-4087-9688-c07d92f3bcc9", + HTTPStatus.OK, + None, + "v2", + "service_keys", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object( + sys, + "argv", + ["main", "create_service_key", json.dumps(dict(service_instance_guid="service_instance_id", name="name-127"))], + ) + def test_main_create_service_key(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.post.return_value = self.mock_response( + "/v2/service_keys", HTTPStatus.CREATED, None, "v2", "service_keys", "POST_response.json" + ) + main.main() + self.client.post.assert_called_with( + self.client.post.return_value.url, json=dict(service_instance_guid="service_instance_id", name="name-127") + ) + + @patch.object(sys, "argv", ["main", "delete_service_key", "67755c27-28ed-4087-9688-c07d92f3bcc9"]) + def test_main_delete_service_key(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.delete.return_value = self.mock_response( + "/v2/service_keys/67755c27-28ed-4087-9688-c07d92f3bcc9", HTTPStatus.NO_CONTENT, None + ) + main.main() + self.client.delete.assert_called_with(self.client.delete.return_value.url) + main.main() diff --git a/tests/v2/test_service_plan_visibilities.py b/tests/v2/test_service_plan_visibilities.py new file mode 100644 index 0000000..9a70a57 --- /dev/null +++ b/tests/v2/test_service_plan_visibilities.py @@ -0,0 +1,122 @@ +import json +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestServicePlanVisibilities(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/service_plan_visibilities?q=space_guid%3Aspace_guid", + HTTPStatus.OK, + None, + "v2", + "service_plan_visibilities", + "GET_response.json", + ) + cpt = reduce( + lambda increment, _: increment + 1, self.client.v2.service_plan_visibilities.list(space_guid="space_guid"), 0 + ) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/service_plan_visibilities/guid", HTTPStatus.OK, None, "v2", "service_plan_visibilities", "GET_{id}_response.json" + ) + result = self.client.v2.service_plan_visibilities.get("guid") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v2/service_plan_visibilities", HTTPStatus.CREATED, None, "v2", "service_plan_visibilities", "POST_response.json" + ) + service_plan_visibilities = self.client.v2.service_plan_visibilities.create("service_plan_guid", "organization_guid") + self.client.post.assert_called_with( + self.client.post.return_value.url, + json=dict(service_plan_guid="service_plan_guid", organization_guid="organization_guid"), + ) + self.assertIsNotNone(service_plan_visibilities) + + def test_update(self): + self.client.put.return_value = self.mock_response( + "/v2/service_plan_visibilities/guid", HTTPStatus.OK, None, "v2", "service_plan_visibilities", "PUT_{id}_response.json" + ) + service_plan_visibilities = self.client.v2.service_plan_visibilities.update( + "guid", service_plan_guid="service_plan_guid", organization_guid="organization_guid" + ) + self.client.put.assert_called_with( + self.client.put.return_value.url, + json=dict(service_plan_guid="service_plan_guid", organization_guid="organization_guid"), + ) + self.assertIsNotNone(service_plan_visibilities) + + def test_delete(self): + self.client.delete.return_value = self.mock_response("/v2/service_plan_visibilities/guid", HTTPStatus.NO_CONTENT, None) + self.client.v2.service_plan_visibilities.remove("guid") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object( + sys, + "argv", + [ + "main", + "create_service_plan_visibility", + json.dumps(dict(service_plan_guid="service-plan-id", organization_guid="organization-id")), + ], + ) + def test_main_create_service_plan_visibility(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.post.return_value = self.mock_response( + "/v2/service_plan_visibilities", HTTPStatus.CREATED, None, "v2", "service_plan_visibilities", "POST_response.json" + ) + main.main() + self.client.post.assert_called_with( + self.client.post.return_value.url, + json=dict(service_plan_guid="service-plan-id", organization_guid="organization-id"), + ) + + @patch.object(sys, "argv", ["main", "list_service_plan_visibilities"]) + def test_main_list_service_plan_visibilities(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_plan_visibilities", HTTPStatus.OK, None, "v2", "service_plan_visibilities", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_service_plan_visibility", "a353104b-1290-418c-bc03-0e647afd0853"]) + def test_main_get_service_plan_visibility(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_plan_visibilities/a353104b-1290-418c-bc03-0e647afd0853", + HTTPStatus.OK, + None, + "v2", + "service_plan_visibilities", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "delete_service_plan_visibility", "906775ea-622e-4bc7-af5d-9aab3b652f81"]) + def test_main_delete_service_plan_visibility(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.delete.return_value = self.mock_response( + "/v2/service_plan_visibilities/906775ea-622e-4bc7-af5d-9aab3b652f81", HTTPStatus.NO_CONTENT, None + ) + main.main() + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v2/test_service_plans.py b/tests/v2/test_service_plans.py new file mode 100644 index 0000000..cc2c55a --- /dev/null +++ b/tests/v2/test_service_plans.py @@ -0,0 +1,98 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call, patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestServicePlans(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/service_plans?q=service_guid%3Aservice_id", HTTPStatus.OK, None, "v2", "service_plans", "GET_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.service_plans.list(service_guid="service_id"), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/service_plans/plan_id", HTTPStatus.OK, None, "v2", "service_plans", "GET_{id}_response.json" + ) + result = self.client.v2.service_plans.get("plan_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_list_instances(self): + self.client.get.return_value = self.mock_response( + "/v2/service_plans/plan_id/service_instances?q=space_guid%3Aspace_id", + HTTPStatus.OK, + None, + "v2", + "apps", + "GET_{id}_routes_response.json", + ) + cpt = reduce( + lambda increment, _: increment + 1, self.client.v2.service_plans.list_instances("plan_id", space_guid="space_id"), 0 + ) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response("/v2/service_plans/plan_id", HTTPStatus.OK, None, "v2", "service_plans", "GET_{id}_response.json"), + self.mock_response( + "/v2/services/6a4abae6-93e0-438b-aaa2-5ae67f3a069d", + HTTPStatus.OK, + None, + "v2", + "services", + "GET_{id}_response.json", + ), + self.mock_response( + "/v2/service_plans/5d8f3b0f-6b5b-487f-8fed-4c2d9b812a72/service_instances", + HTTPStatus.OK, + None, + "v2", + "service_instances", + "GET_response.json", + ), + ] + service_plan = self.client.v2.service_plans.get("plan_id") + + self.assertIsNotNone(service_plan.service()) + cpt = reduce(lambda increment, _: increment + 1, service_plan.service_instances(), 0) + self.assertEqual(cpt, 1) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) + + @patch.object(sys, "argv", ["main", "list_service_plans"]) + def test_main_list_service_plans(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_plans", HTTPStatus.OK, None, "v2", "service_plans", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_service_plan", "5d8f3b0f-6b5b-487f-8fed-4c2d9b812a72"]) + def test_main_get_service_plan(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/service_plans/5d8f3b0f-6b5b-487f-8fed-4c2d9b812a72", + HTTPStatus.OK, + None, + "v2", + "service_plans", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v2/test_services.py b/tests/v2/test_services.py new file mode 100644 index 0000000..6c28106 --- /dev/null +++ b/tests/v2/test_services.py @@ -0,0 +1,73 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call, patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestServices(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/services?q=label%3Asome_label", HTTPStatus.OK, None, "v2", "services", "GET_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.services.list(label="some_label"), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/services/service_id", HTTPStatus.OK, None, "v2", "services", "GET_{id}_response.json" + ) + result = self.client.v2.services.get("service_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response("/v2/services/service_id", HTTPStatus.OK, None, "v2", "services", "GET_{id}_response.json"), + self.mock_response( + "/v2/services/2c883dbb-a726-4ecf-a0b7-d65588897e7f/service_plans", + HTTPStatus.OK, + None, + "v2", + "service_plans", + "GET_response.json", + ), + ] + service = self.client.v2.services.get("service_id") + cpt = reduce(lambda increment, _: increment + 1, service.service_plans(), 0) + self.assertEqual(cpt, 1) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) + + @patch.object(sys, "argv", ["main", "list_services"]) + def test_main_list_services(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/services", HTTPStatus.OK, None, "v2", "services", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_service", "2c883dbb-a726-4ecf-a0b7-d65588897e7f"]) + def test_main_get_service(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/services/2c883dbb-a726-4ecf-a0b7-d65588897e7f", + HTTPStatus.OK, + None, + "v2", + "services", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v2/test_spaces.py b/tests/v2/test_spaces.py new file mode 100644 index 0000000..42ac1f2 --- /dev/null +++ b/tests/v2/test_spaces.py @@ -0,0 +1,88 @@ +import sys +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import call, patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + + +class TestSpaces(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v2/spaces?q=organization_guid%3Aorg_id", HTTPStatus.OK, None, "v2", "spaces", "GET_response.json" + ) + cpt = reduce(lambda increment, _: increment + 1, self.client.v2.spaces.list(organization_guid="org_id"), 0) + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(cpt, 1) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v2/spaces/space_id", HTTPStatus.OK, None, "v2", "spaces", "GET_{id}_response.json" + ) + result = self.client.v2.spaces.get("space_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_entity(self): + self.client.get.side_effect = [ + self.mock_response("/v2/spaces/space_id", HTTPStatus.OK, None, "v2", "spaces", "GET_{id}_response.json"), + self.mock_response( + "/v2/organizations/d7d77408-a250-45e3-8de5-71fcf199bbab", + HTTPStatus.OK, + None, + "v2", + "organizations", + "GET_{id}_response.json", + ), + self.mock_response( + "/v2/spaces/2d745a4b-67e3-4398-986e-2adbcf8f7ec9/apps", HTTPStatus.OK, None, "v2", "apps", "GET_response.json" + ), + self.mock_response( + "/v2/spaces/2d745a4b-67e3-4398-986e-2adbcf8f7ec9/service_instances", + HTTPStatus.OK, + None, + "v2", + "service_instances", + "GET_response.json", + ), + ] + space = self.client.v2.spaces.get("space_id") + self.assertIsNotNone(space.organization()) + cpt = reduce(lambda increment, _: increment + 1, space.apps(), 0) + self.assertEqual(cpt, 3) + cpt = reduce(lambda increment, _: increment + 1, space.service_instances(), 0) + self.assertEqual(cpt, 1) + self.client.get.assert_has_calls([call(side_effect.url) for side_effect in self.client.get.side_effect], any_order=False) + + def test_delete_unmapped_routes(self): + self.client.delete.return_value = self.mock_response( + "/v2/spaces/space_id/unmapped_routes", HTTPStatus.NO_CONTENT, None) + self.client.v2.spaces.delete_unmapped_routes("space_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object(sys, "argv", ["main", "list_spaces"]) + def test_main_list_spaces(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/spaces", HTTPStatus.OK, None, "v2", "spaces", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_space", "2d745a4b-67e3-4398-986e-2adbcf8f7ec9"]) + def test_main_get_spaces(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v2/spaces/2d745a4b-67e3-4398-986e-2adbcf8f7ec9", HTTPStatus.OK, None, "v2", "spaces", "GET_{id}_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v3/__init__.py b/tests/v3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py new file mode 100644 index 0000000..dbe8fb6 --- /dev/null +++ b/tests/v3/test_apps.py @@ -0,0 +1,207 @@ +import unittest +import yaml +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.common_objects import JsonObject, Pagination +from cloudfoundry_client.v3.entities import Entity + + +class TestApps(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response("/v3/apps", HTTPStatus.OK, None, "v3", "apps", "GET_response.json") + all_applications = [application for application in self.client.v3.apps.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_applications)) + self.assertEqual(all_applications[0]["name"], "my_app") + self.assertIsInstance(all_applications[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_response.json" + ) + application = self.client.v3.apps.get("app_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my_app", application["name"]) + self.assertIsInstance(application, Entity) + + def test_get_then_space(self): + get_app = self.mock_response("/v3/apps/app_id", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_response.json") + get_space = self.mock_response( + "/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576", HTTPStatus.OK, None, "v3", "spaces", "GET_{id}_response.json" + ) + self.client.get.side_effect = [get_app, get_space] + space = self.client.v3.apps.get("app_id").space() + # self.client.get.assert_has_calls([call(get_app.url), + # call(get_space.url)], + # any_order=False) + self.assertEqual("my-space", space["name"]) + + def test_get_then_start(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_response.json" + ) + self.client.post.return_value = self.mock_response( + "/v3/apps/app_id/actions/start", HTTPStatus.OK, None, "v3", "apps", "POST_{id}_actions_start_response.json" + ) + + app = self.client.v3.apps.get("app_id").start() + self.client.get.assert_called_with(self.client.get.return_value.url) + self.client.post.assert_called_with(self.client.post.return_value.url, files=None, json=None) + self.assertEqual("my_app", app["name"]) + self.assertIsInstance(app, Entity) + + def test_get_then_environment_variables(self): + get_app = self.mock_response("/v3/apps/app_id", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_response.json") + get_environment_variables = self.mock_response( + "/v3/apps/app_id/environment_variables", + HTTPStatus.OK, + None, + "v3", + "apps", + "GET_{id}_environment_variables_response.json", + ) + self.client.get.side_effect = [get_app, get_environment_variables] + app = self.client.v3.apps.get("app_id") + environment_variables = app.environment_variables() + self.assertIsInstance(environment_variables, dict) + self.assertEqual("production", environment_variables["var"]["RAILS_ENV"]) + + def test_restart(self): + self.client.post.return_value = self.mock_response( + "/v3/apps/app_id/actions/restart", HTTPStatus.OK, None, "v3", "apps", + "GET_{id}_response.json" + ) + + app = self.client.v3.apps.restart("app_id") + self.assertIsInstance(app, JsonObject) + self.assertEqual("my_app", app["name"]) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/apps/app_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.apps.remove("app_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_get_env(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id/env", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_env_response.json" + ) + env = self.client.v3.apps.get_env("app_id") + 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" + ) + routes: list[dict] = [revision for revision in self.client.v3.apps.list_routes("app_id")] + self.assertEqual(1, len(routes)) + + self.assertEqual(routes[0]["destinations"][0]["guid"], "385bf117-17f5-4689-8c5c-08c6cc821fed") + + def test_get_include_space_and_org(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id?include=space.organization", + HTTPStatus.OK, + None, + "v3", + "apps", + "GET_{id}_response_include_space_and_org.json" + ) + space = self.client.v3.apps.get("app_id", include="space.organization").space() + self.client.get.assert_called_with(self.client.get.return_value.url) + org = space.organization() + self.assertEqual("my_space", space["name"]) + self.assertIsInstance(space, Entity) + self.assertEqual("my_organization", org["name"]) + self.assertIsInstance(org, Entity) + + def test_list_include_space(self): + self.client.get.return_value = self.mock_response( + "/v3/apps?include=space", HTTPStatus.OK, None, "v3", "apps", "GET_response_include_space.json" + ) + all_spaces = [app.space() for app in self.client.v3.apps.list(include="space")] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_spaces)) + self.assertEqual(all_spaces[0]["name"], "my_space") + 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: list[dict] | None = 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: list[str] | None = application.get("services") + self.assertIsInstance(application_services, list) + self.assertEqual(len(application_services), 1) + self.assertEqual(application_services[0], "my-service") + application_routes: list[dict | str] | None = 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") + + 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.list_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_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.list_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) diff --git a/tests/v3/test_audit_events.py b/tests/v3/test_audit_events.py new file mode 100644 index 0000000..239812b --- /dev/null +++ b/tests/v3/test_audit_events.py @@ -0,0 +1,39 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestAuditEvents(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/audit_events", + HTTPStatus.OK, + None, + "v3", "audit_events", "GET_response.json" + ) + all_audit_events = [audit_event for audit_event in self.client.v3.audit_events.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(1, len(all_audit_events)) + self.assertEqual(all_audit_events[0]["type"], "audit.app.update") + self.assertIsInstance(all_audit_events[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/audit_events/audit-event-id", + HTTPStatus.OK, + None, + "v3", "audit_events", "GET_{id}_response.json" + ) + result = self.client.v3.audit_events.get("audit-event-id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) diff --git a/tests/v3/test_buildpacks.py b/tests/v3/test_buildpacks.py new file mode 100644 index 0000000..cec5228 --- /dev/null +++ b/tests/v3/test_buildpacks.py @@ -0,0 +1,163 @@ +import sys +import unittest +from http import HTTPStatus +from unittest.mock import patch, mock_open + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestBuildpacks(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/buildpacks", HTTPStatus.OK, None, "v3", "buildpacks", "GET_response.json" + ) + all_buildpacks = [buildpack for buildpack in self.client.v3.buildpacks.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(1, len(all_buildpacks)) + self.assertEqual(all_buildpacks[0]["name"], "my-buildpack") + self.assertIsInstance(all_buildpacks[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/buildpacks/buildpack_id", HTTPStatus.OK, None, "v3", "buildpacks", "GET_{id}_response.json" + ) + result = self.client.v3.buildpacks.get("buildpack_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/buildpacks/buildpack_id", HTTPStatus.OK, None, "v3", "buildpacks", "PATCH_{id}_response.json" + ) + result = self.client.v3.buildpacks.update("buildpack_id", "ruby_buildpack", enabled=True, position=42, stack="windows64") + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "locked": False, + "name": "ruby_buildpack", + "enabled": True, + "position": 42, + "stack": "windows64", + }, + ) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/buildpacks", HTTPStatus.OK, None, "v3", "buildpacks", "POST_response.json" + ) + result = self.client.v3.buildpacks.create("ruby_buildpack", enabled=True, position=42, stack="windows64") + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={ + "locked": False, + "name": "ruby_buildpack", + "enabled": True, + "position": 42, + "stack": "windows64", + }, + ) + self.assertIsNotNone(result) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/buildpacks/buildpack_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.buildpacks.remove("buildpack_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object(sys, "argv", ["main", "list_buildpacks"]) + def test_main_list_buildpacks(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/buildpacks", HTTPStatus.OK, None, "v3", "buildpacks", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_buildpack", "6e72c33b-dff0-4020-8603-bcd8a4eb05e4"]) + def test_main_get_buildpack(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/buildpacks/6e72c33b-dff0-4020-8603-bcd8a4eb05e4", + HTTPStatus.OK, + None, + "v3", + "buildpacks", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + def test_upload_buildpack(self): + self.client.post.return_value = self.mock_response( + "/v3/buildpacks/buildpack_id/upload", + HTTPStatus.ACCEPTED, + {"Location": "https://somewhere.org/v3/jobs/job_id", "Content-Type": "application/json"}, + "v3", + "buildpacks", + "POST_response.json", + ) + + self.client.get.side_effect = [ + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + {"Content-Type": "application/json"}, + "v3", + "jobs", + "GET_{id}_complete_response.json", + ), + self.mock_response( + "/v3/buildpacks/buildpack_id", + HTTPStatus.OK, + {"Content-Type": "application/json"}, + "v3", + "buildpacks", + "GET_{id}_response.json", + ), + ] + + with patch("cloudfoundry_client.v3.entities.open", mock_open(read_data="ZipContent")) as m: + with patch("cloudfoundry_client.v3.jobs.JobManager.wait_for_job_completion") as job_mock: + result = self.client.v3.buildpacks.upload("buildpack_id", "/path/to/buildpack.zip") + + # Check that we made the post call to upload the buildpack + self.client.post.assert_called_with( + self.client.post.return_value.url, files={"bits": ("/path/to/buildpack.zip", m.return_value)}, json=None + ) + # Check that we wait for job completion + job_mock.assert_called_once() + # We are doing upload->waitForJob->getbuildpack to get a fresh buildpack entity after the job finished + # We then rewrite the job information into the new buildpack entity since it is missing on get endpoint + # So check job link and function is also in the returned entity when we waited for the job. + self.assertIsNotNone(result["links"]["job"]) + self.assertIsNotNone(result.job) + + def test_upload_buildpack_dont_wait_for_job_completion(self): + self.client.post.return_value = self.mock_response( + "/v3/buildpacks/buildpack_id/upload", + HTTPStatus.ACCEPTED, + {"Location": "https://somewhere.org/v3/jobs/job_guid"}, + "v3", + "buildpacks", + "POST_response.json", + ) + + with patch("cloudfoundry_client.v3.entities.open", mock_open(read_data="ZipContent")) as m: + result = self.client.v3.buildpacks.upload("buildpack_id", "/path/to/buildpack.zip", asynchronous=True) + + self.client.post.assert_called_with( + self.client.post.return_value.url, files={"bits": ("/path/to/buildpack.zip", m.return_value)}, json=None + ) + self.client.get.assert_not_called() + + self.assertIsNotNone(result) diff --git a/tests/v3/test_domains.py b/tests/v3/test_domains.py new file mode 100644 index 0000000..57a28f9 --- /dev/null +++ b/tests/v3/test_domains.py @@ -0,0 +1,145 @@ +import sys +import unittest +from http import HTTPStatus +from unittest.mock import patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.domains import Domain +from cloudfoundry_client.v3.entities import Entity, ToManyRelationship, ToOneRelationship + + +class TestDomains(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/domains", HTTPStatus.OK, None, "v3", "domains", "GET_response.json" + ) + all_domains = [domain for domain in self.client.v3.domains.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(1, len(all_domains)) + self.assertEqual(all_domains[0]["name"], "test-domain.com") + self.assertIsInstance(all_domains[0], Entity) + for domain in all_domains: + self.assertIsInstance(domain, Domain) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/domains/domain_id", HTTPStatus.OK, None, "v3", "domains", "GET_{id}_response.json" + ) + result = self.client.v3.domains.get("domain_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Domain) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/domains/domain_id", HTTPStatus.OK, None, "v3", "domains", "PATCH_{id}_response.json" + ) + result = self.client.v3.domains.update("domain_id") + self.client.patch.assert_called_with( + self.client.patch.return_value.url, json={"metadata": {"labels": None, "annotations": None}} + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Domain) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/domains", HTTPStatus.OK, None, "v3", "domains", "POST_response.json" + ) + result = self.client.v3.domains.create( + "domain_id", + internal=False, + organization=ToOneRelationship("organization-guid"), + shared_organizations=ToManyRelationship("other-org-guid-1", "other-org-guid-2"), + meta_labels=None, + meta_annotations=None, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={ + "name": "domain_id", + "internal": False, + "relationships": { + "organization": {"data": {"guid": "organization-guid"}}, + "shared_organizations": { + "data": [ + {"guid": "other-org-guid-1"}, + {"guid": "other-org-guid-2"}, + ] + }, + }, + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Domain) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/domains/domain_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.domains.remove("domain_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_list_domains_for_org(self): + self.client.get.return_value = self.mock_response( + "/v3/organizations/org_id/domains", HTTPStatus.OK, None, "v3", "domains", "GET_response.json" + ) + all_domains = [domain for domain in self.client.v3.domains.list_domains_for_org("org_id")] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(1, len(all_domains)) + self.assertEqual(all_domains[0]["name"], "test-domain.com") + for domain in all_domains: + self.assertIsInstance(domain, Domain) + + def test_share_domain(self): + self.client.post.return_value = self.mock_response( + "/v3/domains/domain_id/relationships/shared_organizations", + HTTPStatus.CREATED, + None, + "v3", + "domains", + "POST_{id}_relationships_shared_organizations_response.json", + ) + result = self.client.v3.domains.share_domain( + "domain_id", ToManyRelationship("organization-guid-1", "organization-guid-2") + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={"data": [{"guid": "organization-guid-1"}, {"guid": "organization-guid-2"}]}, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, ToManyRelationship) + result.guids[0] = "organization-guid-1" + result.guids[1] = "organization-guid-1" + + def test_unshare_domain(self): + self.client.delete.return_value = self.mock_response( + "/v3/domains/domain_id/relationships/shared_organizations/org_id", HTTPStatus.NO_CONTENT, None + ) + self.client.v3.domains.unshare_domain("domain_id", "org_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object(sys, "argv", ["main", "list_domains"]) + def test_main_list_domains(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/domains", HTTPStatus.OK, None, "v3", "domains", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_domain", "3a5d3d89-3f89-4f05-8188-8a2b298c79d5"]) + def test_main_get_domain(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/domains/3a5d3d89-3f89-4f05-8188-8a2b298c79d5", HTTPStatus.OK, None, "v3", "domains", "GET_{id}_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v3/test_droplets.py b/tests/v3/test_droplets.py new file mode 100644 index 0000000..ebfd2be --- /dev/null +++ b/tests/v3/test_droplets.py @@ -0,0 +1,136 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestDroplets(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/droplets", + HTTPStatus.OK, + None, + "v3", "droplets", "POST_response.json" + ) + result = self.client.v3.droplets.create( + app_guid="app-guid", + process_types={ + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + 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" + } + } + }, + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "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/droplets?source_guid=droplet_id", + HTTPStatus.OK, + None, + "v3", "droplets", "POST_response.json" + ) + result = self.client.v3.droplets.copy( + droplet_guid="droplet_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/droplets", + HTTPStatus.OK, + None, + "v3", "droplets", "GET_response.json" + ) + all_droplets = [droplet for droplet in self.client.v3.droplets.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_droplets)) + self.assertEqual(all_droplets[0]["state"], "STAGED") + for droplet in all_droplets: + self.assertIsInstance(droplet, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/droplets/route_id", + HTTPStatus.OK, + None, + "v3", "droplets", "GET_{id}_response.json" + ) + result = self.client.v3.droplets.get("route_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/droplets/droplet_id", + HTTPStatus.OK, + None, + "v3", "droplets", "PATCH_{id}_response.json" + ) + result = self.client.v3.droplets.update( + "droplet_id", + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "metadata": { + "labels": {"key": "value"}, + "annotations": {"note": "detailed information"} + } + } + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/droplets/droplet_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.droplets.remove("droplet_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_entities.py b/tests/v3/test_entities.py new file mode 100644 index 0000000..65924ef --- /dev/null +++ b/tests/v3/test_entities.py @@ -0,0 +1,85 @@ +import unittest +from functools import reduce +from http import HTTPStatus +from unittest.mock import MagicMock + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import EntityManager, Entity + + +class TestEntities(unittest.TestCase, AbstractTestCase): + def test_len(self): + client = MagicMock() + client.get.return_value = self.mock_response("/fake/something", HTTPStatus.OK, None, "v3", "apps", "GET_response.json") + + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + + cpt = entity_manager.list().total_results + + self.assertEqual(cpt, 3) + client.get.assert_called_with(client.get.return_value.url) + + def test_entity_manager_is_a_generator(self): + client = MagicMock() + client.get.return_value = self.mock_response("/fake/something", HTTPStatus.OK, None, "v3", "apps", "GET_response.json") + + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + + self.assertIsNotNone(getattr(entity_manager, "__iter__", None)) + self.assertIsNotNone(getattr(entity_manager.__iter__, "__call__", None)) + generator = entity_manager.__iter__() + self.assertIsNotNone(getattr(generator, "__next__", None)) + self.assertIsNotNone(getattr(generator.__next__, "__call__", None)) + + def test_entity_list_is_a_generator(self): + client = MagicMock() + client.get.return_value = self.mock_response("/fake/something", HTTPStatus.OK, None, "v3", "apps", "GET_response.json") + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + + generator = entity_manager.list() + + self.assertIsNotNone(getattr(generator, "__next__", None)) + self.assertIsNotNone(getattr(generator.__next__, "__call__", None)) + + def test_elements_are_entities(self): + client = MagicMock() + client.get.return_value = self.mock_response("/fake/something", HTTPStatus.OK, None, "v3", "apps", "GET_response.json") + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake/something") + + entities_list = entity_manager.list() + + for entity in entities_list: + self.assertIsInstance(entity, Entity) + + def test_list_pagination(self): + client = MagicMock() + + entity_manager = EntityManager(self.TARGET_ENDPOINT, client, "/fake") + first_response = self.mock_response( + "/fake", + HTTPStatus.OK, + None, + "v3", "fake", + "GET_multi_page_0_response.json", + ) + second_response = self.mock_response( + "/fake/last?order-direction=asc&page=2&results-per-page=50", + HTTPStatus.OK, + None, + "v3", "fake", + "GET_multi_page_1_response.json", + ) + + client.get.side_effect = [first_response, second_response] + + guids = reduce( + lambda c, entity: c.append(entity["guid"]) or c, + entity_manager.list(), + [], + ) + self.assertEqual(guids, [ + "1cb006ee-fb05-47e1-b541-c34179ddc446", + "02b4ec9b-94c7-4468-9c23-4e906191a0f8", + "1cb006ee-fb05-47e1-b541-c34179ddc447", + "02b4ec9b-94c7-4468-9c23-4e906191a0f9", + ]) diff --git a/tests/v3/test_feature_flags.py b/tests/v3/test_feature_flags.py new file mode 100644 index 0000000..25fc2b6 --- /dev/null +++ b/tests/v3/test_feature_flags.py @@ -0,0 +1,41 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase + + +class TestFeatureFlags(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/feature_flags", HTTPStatus.OK, None, "v3", "feature_flags", "GET_response.json" + ) + all_feature_flags = [feature_flag for feature_flag in self.client.v3.feature_flags.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_feature_flags)) + self.assertEqual(all_feature_flags[0]["name"], "my_feature_flag") + self.assertEqual(all_feature_flags[1]["name"], "my_second_feature_flag") + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/feature_flags/feature_flag_name", HTTPStatus.OK, None, "v3", "feature_flags", "GET_{id}_response.json" + ) + feature_flag = self.client.v3.feature_flags.get("feature_flag_name") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my_feature_flag", feature_flag["name"]) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/feature_flags/feature_flag_name", HTTPStatus.OK, None, "v3", "feature_flags", "PATCH_{id}_response.json" + ) + result = self.client.v3.feature_flags.update("feature_flag_name") + self.client.patch.assert_called_with( + self.client.patch.return_value.url, json={"enabled": True, "custom_error_message": None} + ) + self.assertIsNotNone(result) diff --git a/tests/v3/test_isolation_segments.py b/tests/v3/test_isolation_segments.py new file mode 100644 index 0000000..344da89 --- /dev/null +++ b/tests/v3/test_isolation_segments.py @@ -0,0 +1,138 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity, ToManyRelationship + + +class TestIsolationSegments(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/isolation_segments", HTTPStatus.OK, None, "v3", "isolation_segments", "GET_response.json" + ) + all_isolation_segments = [isolation_segment for isolation_segment in self.client.v3.isolation_segments.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(5, len(all_isolation_segments)) + self.assertEqual(all_isolation_segments[0]["name"], "an_isolation_segment") + self.assertIsInstance(all_isolation_segments[0], Entity) + for isolation_segment in all_isolation_segments: + self.assertIsInstance(isolation_segment, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/isolation_segments/isolation_segment_id", + HTTPStatus.OK, + None, + "v3", + "isolation_segments", + "GET_{id}_response.json", + ) + result = self.client.v3.isolation_segments.get("isolation_segment_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/isolation_segments/isolation_segment_id", + HTTPStatus.OK, + None, + "v3", + "isolation_segments", + "PATCH_{id}_response.json", + ) + result = self.client.v3.isolation_segments.update("isolation_segment_id", "new-name", meta_labels=dict(key="value")) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={"name": "new-name", "metadata": {"labels": {"key": "value"}}}, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/isolation_segments", HTTPStatus.OK, None, "v3", "isolation_segments", "POST_response.json" + ) + result = self.client.v3.isolation_segments.create( + "isolation_segment_id", + meta_labels=dict(key_label="value_label"), + meta_annotations=dict(key_annotation="value_annotation"), + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={ + "name": "isolation_segment_id", + "metadata": {"labels": {"key_label": "value_label"}, "annotations": {"key_annotation": "value_annotation"}}, + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response( + "/v3/isolation_segments/isolation_segment_id", HTTPStatus.NO_CONTENT, None + ) + self.client.v3.isolation_segments.remove("isolation_segment_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_entitle_organizations(self): + self.client.post.return_value = self.mock_response( + "/v3/isolation_segments/isolation_segment_id/relationships/organizations", + HTTPStatus.OK, + None, + "v3", + "isolation_segments", + "POST_{id}_relationships_organizations_response.json", + ) + result = self.client.v3.isolation_segments.entitle_organizations("isolation_segment_id", "org_id_1", "org_id_2") + self.client.post.assert_called_with( + self.client.post.return_value.url, files=None, json={"data": [{"guid": "org_id_1"}, {"guid": "org_id_2"}]} + ) + self.assertIsInstance(result, ToManyRelationship) + self.assertEqual(2, len(result.guids)) + self.assertIsNotNone(result["links"]) + + def test_revoke_organization(self): + self.client.delete.return_value = self.mock_response( + "/v3/isolation_segments/isolation_segment_id/relationships/organizations/org_id", HTTPStatus.NO_CONTENT, None + ) + self.client.v3.isolation_segments.revoke_organization("isolation_segment_id", "org_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_list_entitled_organizations(self): + self.client.get.return_value = self.mock_response( + "/v3/isolation_segments/isolation_segment_id/relationships/organizations", + HTTPStatus.OK, + None, + "v3", + "isolation_segments", + "GET_{id}_relationships_organizations_response.json", + ) + result = self.client.v3.isolation_segments.list_entitled_organizations("isolation_segment_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsInstance(result, ToManyRelationship) + self.assertEqual(2, len(result.guids)) + self.assertIsNotNone(result["links"]) + + def test_list_entitled_spaces(self): + self.client.get.return_value = self.mock_response( + "/v3/isolation_segments/isolation_segment_id/relationships/spaces", + HTTPStatus.OK, + None, + "v3", + "isolation_segments", + "GET_{id}_relationships_spaces_response.json", + ) + result = self.client.v3.isolation_segments.list_entitled_spaces("isolation_segment_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsInstance(result, ToManyRelationship) + self.assertEqual(2, len(result.guids)) + self.assertIsNotNone(result["links"]) diff --git a/tests/v3/test_jobs.py b/tests/v3/test_jobs.py new file mode 100644 index 0000000..e62ab29 --- /dev/null +++ b/tests/v3/test_jobs.py @@ -0,0 +1,115 @@ +import unittest +from http import HTTPStatus +from unittest.mock import patch, call +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.jobs import JobTimeout + + +class TestJobs(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ) + job = self.client.v3.jobs.get("job_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(job) + + @patch("time.sleep", return_value=None) + def test_wait_for_job_completion(self, sleepmock): + self.client.get.side_effect = [ + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ), + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ), + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_complete_response.json", + ), + ] + + job = self.client.v3.jobs.wait_for_job_completion("job_id") + + assert self.client.get.call_count == 3 + self.assertIsNotNone(job) + + def test_wait_for_job_completion_does_exponential_backoff(self): + self.client.get.side_effect = [ + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ), + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ), + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ), + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_complete_response.json", + ), + ] + + with patch("time.sleep", return_value=None) as m: + self.client.v3.jobs.wait_for_job_completion("job_id") + m.assert_has_calls([call(1), call(2), call(4)]) + + @patch("time.sleep", return_value=None) + def test_wait_for_job_completion_has_timeout(self, sleepmock): + self.client.get.return_value = self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ) + + with self.assertRaises(JobTimeout): + self.client.v3.jobs.wait_for_job_completion("job_id", timeout=0.0001) diff --git a/tests/v3/test_organization_quotas.py b/tests/v3/test_organization_quotas.py new file mode 100644 index 0000000..c7cc0f8 --- /dev/null +++ b/tests/v3/test_organization_quotas.py @@ -0,0 +1,136 @@ +import sys +import unittest +from http import HTTPStatus +from unittest.mock import patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity, ToManyRelationship +from cloudfoundry_client.v3.organization_quotas import AppsQuota, DomainsQuota, RoutesQuota, ServicesQuota + + +class TestOrganizationQuotas(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/organization_quotas", HTTPStatus.OK, None, "v3", "organization_quotas", "GET_response.json" + ) + all_quotas = [quota for quota in self.client.v3.organization_quotas.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_quotas)) + self.assertEqual(all_quotas[0]["name"], "don-quixote") + self.assertIsInstance(all_quotas[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/organization_quotas/quota-guid", HTTPStatus.OK, None, "v3", "organization_quotas", "GET_{id}_response.json" + ) + quota = self.client.v3.organization_quotas.get("quota-guid") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("don-quixote", quota["name"]) + self.assertIsInstance(quota, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/organization_quotas/quota_id", HTTPStatus.OK, None, "v3", "organization_quotas", "PATCH_{id}_response.json" + ) + result = self.client.v3.organization_quotas.update( + "quota_id", + "don-quixote", + apps_quota=AppsQuota(total_memory_in_mb=5120, per_process_memory_in_mb=1024, total_instances=10, per_app_tasks=5), + services_quota=ServicesQuota(paid_services_allowed=True, total_service_instances=10, total_service_keys=20), + routes_quota=RoutesQuota(total_routes=8, total_reserved_ports=4), + domains_quota=DomainsQuota(total_domains=7), + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "name": "don-quixote", + "apps": {"total_memory_in_mb": 5120, "per_process_memory_in_mb": 1024, "total_instances": 10, "per_app_tasks": 5}, + "services": {"paid_services_allowed": True, "total_service_instances": 10, "total_service_keys": 20}, + "routes": {"total_routes": 8, "total_reserved_ports": 4}, + "domains": {"total_domains": 7}, + }, + ) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/organization_quotas", HTTPStatus.OK, None, "v3", "organization_quotas", "POST_response.json" + ) + result = self.client.v3.organization_quotas.create( + "don-quixote", + apps_quota=AppsQuota(total_memory_in_mb=5120, per_process_memory_in_mb=1024, total_instances=10, per_app_tasks=5), + services_quota=ServicesQuota(paid_services_allowed=True, total_service_instances=10, total_service_keys=20), + routes_quota=RoutesQuota(total_routes=8, total_reserved_ports=4), + domains_quota=DomainsQuota(total_domains=7), + assigned_organizations=ToManyRelationship("assigned-org"), + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={ + "name": "don-quixote", + "apps": {"total_memory_in_mb": 5120, "per_process_memory_in_mb": 1024, "total_instances": 10, "per_app_tasks": 5}, + "services": {"paid_services_allowed": True, "total_service_instances": 10, "total_service_keys": 20}, + "routes": {"total_routes": 8, "total_reserved_ports": 4}, + "domains": {"total_domains": 7}, + "relationships": {"organizations": {"data": [{"guid": "assigned-org"}]}}, + }, + ) + self.assertIsNotNone(result) + + def test_apply_to_organizations(self): + self.client.post.return_value = self.mock_response( + "/v3/organization_quotas/quota_id/relationships/organizations", + HTTPStatus.OK, + None, + "v3", + "organization_quotas", + "POST_{id}_organizations_response.json", + ) + result = self.client.v3.organization_quotas.apply_to_organizations( + "quota_id", + organizations=ToManyRelationship("org-guid1", "org-guid2"), + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, files=None, json={"data": [{"guid": "org-guid1"}, {"guid": "org-guid2"}]} + ) + self.assertIsInstance(result, ToManyRelationship) + # Endpoint adds to existing list of orgs so 1 existing + 2 new + self.assertEqual(3, len(result.guids)) + self.assertIsNotNone(result["links"]) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/organization_quotas/quota_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.organization_quotas.remove("quota_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch.object(sys, "argv", ["main", "list_organization_quotas"]) + def test_main_list_organization_quotas(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/organization_quotas", HTTPStatus.OK, None, "v3", "organization_quotas", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_organization_quota", "1d3bf0ec-5806-43c4-b64e-8364dba1086a"]) + def test_main_get_organization_quotas(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/organization_quotas/1d3bf0ec-5806-43c4-b64e-8364dba1086a", + HTTPStatus.OK, + None, + "v3", + "organization_quotas", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v3/test_organizations.py b/tests/v3/test_organizations.py new file mode 100644 index 0000000..e1b90dd --- /dev/null +++ b/tests/v3/test_organizations.py @@ -0,0 +1,189 @@ +import sys +import unittest +from http import HTTPStatus +from unittest.mock import patch + +import cloudfoundry_client.main.main as main +from abstract_test_case import AbstractTestCase + +from cloudfoundry_client.common_objects import Pagination +from cloudfoundry_client.v3.entities import Entity, ToOneRelationship + + +class TestOrganizations(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/organizations", HTTPStatus.OK, None, "v3", "organizations", "GET_response.json" + ) + all_organizations = [organization for organization in self.client.v3.organizations.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_organizations)) + self.assertEqual(all_organizations[0]["name"], "org1") + self.assertIsInstance(all_organizations[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/organizations/organization_id", HTTPStatus.OK, None, "v3", "organizations", "GET_{id}_response.json" + ) + organization = self.client.v3.organizations.get("organization_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my-organization", organization["name"]) + self.assertIsInstance(organization, Entity) + + def test_update_without_optional_parameters(self): + self.client.patch.return_value = self.mock_response( + "/v3/organizations/organization_id", HTTPStatus.OK, None, "v3", "organizations", "PATCH_{id}_response.json" + ) + result = self.client.v3.organizations.update("organization_id", "my-organization") + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={"name": "my-organization"}, + ) + self.assertIsNotNone(result) + + def test_update_with_optional_parameters(self): + self.client.patch.return_value = self.mock_response( + "/v3/organizations/organization_id", HTTPStatus.OK, None, "v3", "organizations", "PATCH_{id}_response.json" + ) + result = self.client.v3.organizations.update( + "organization_id", + "my-organization", + suspended=True, + meta_labels={"label_name": "label_value"}, + meta_annotations={"annotation_name": "annotation_value"} + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "suspended": True, + "name": "my-organization", + "metadata": { + "labels": {"label_name": "label_value"}, + "annotations": {"annotation_name": "annotation_value"} + } + }, + ) + self.assertIsNotNone(result) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/organizations", HTTPStatus.OK, None, "v3", "organizations", "POST_response.json" + ) + result = self.client.v3.organizations.create("my-organization", suspended=False) + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={"name": "my-organization", "suspended": False}, + ) + self.assertIsNotNone(result) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/organizations/organization_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.organizations.remove("organization_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_assign_default_isolation_segment(self): + self.client.patch.return_value = self.mock_response( + "/v3/organizations/organization_id/relationships/default_isolation_segment", + HTTPStatus.OK, + None, + "v3", + "organizations", + "PATCH_{id}_relationships_default_isolation_segment_response.json", + ) + result = self.client.v3.organizations.assign_default_isolation_segment("organization_id", "iso_seg_guid") + self.client.patch.assert_called_with(self.client.patch.return_value.url, json={"data": {"guid": "iso_seg_guid"}}) + self.assertIsNotNone(result) + self.assertIsInstance(result, ToOneRelationship) + self.assertEqual(result.guid, "9d8e007c-ce52-4ea7-8a57-f2825d2c6b39") + + def test_get_default_isolation_segment(self): + self.client.get.return_value = self.mock_response( + "/v3/organizations/organization_id/relationships/default_isolation_segment", + HTTPStatus.OK, + None, + "v3", + "organizations", + "GET_{id}_relationships_default_isolation_segment_response.json", + ) + + result = self.client.v3.organizations.get_default_isolation_segment("organization_id") + + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsInstance(result, ToOneRelationship) + self.assertEqual(result.guid, "9d8e007c-ce52-4ea7-8a57-f2825d2c6b39") + + def test_get_default_domain(self): + self.client.get.return_value = self.mock_response( + "/v3/organizations/organization_id/domains/default", + HTTPStatus.OK, + None, + "v3", + "organizations", + "GET_{id}_default_domain_response.json", + ) + default_domain = self.client.v3.organizations.get_default_domain("organization_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("test-domain.com", default_domain["name"]) + self.assertIsInstance(default_domain, Entity) + + def test_get_usage_summary(self): + self.client.get.return_value = self.mock_response( + "/v3/organizations/organization_id/usage_summary", + HTTPStatus.OK, + None, + "v3", + "organizations", + "GET_{id}_usage_summary_response.json", + ) + self.client.v3.organizations.get_usage_summary("organization_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + + def test_get_domains(self): + self.client.get.return_value = self.mock_response( + "/v3/organizations/organization_id/domains", + HTTPStatus.OK, + {"Content-Type": "application/json"}, + "v3", + "organizations", + "GET_{id}_domains_response.json", + ) + organization_domains_response: Pagination[Entity] = self.client.v3.organizations.list_domains("organization_id") + domains: list[dict] = [domain for domain in organization_domains_response] + print(domains) + self.assertIsInstance(domains, list) + self.assertEqual(len(domains), 1) + domain: dict = domains[0] + self.assertIsInstance(domain, dict) + self.assertEqual(domain.get("guid"), "3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + self.assertEqual(domain.get("name"), "test-domain.com") + + @patch.object(sys, "argv", ["main", "list_organizations"]) + def test_main_list_organizations(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/organizations", HTTPStatus.OK, None, "v3", "organizations", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "get_organization", "24637893-3b77-489d-bb79-8466f0d88b52"]) + def test_main_get_organizations(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/organizations/24637893-3b77-489d-bb79-8466f0d88b52", + HTTPStatus.OK, + None, + "v3", + "organizations", + "GET_{id}_response.json", + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) diff --git a/tests/v3/test_packages.py b/tests/v3/test_packages.py new file mode 100644 index 0000000..e19e13c --- /dev/null +++ b/tests/v3/test_packages.py @@ -0,0 +1,110 @@ +import unittest +from http import HTTPStatus + +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): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + 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", + HTTPStatus.OK, + None, + "v3", "packages", "GET_response.json" + ) + all_packages = [package for package in self.client.v3.packages.list()] + 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 package in all_packages: + self.assertIsInstance(package, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/packages/package_id", + HTTPStatus.OK, + None, + "v3", "packages", "GET_{id}_response.json" + ) + result = self.client.v3.packages.get("package_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list_droplets(self): + self.client.get.return_value = self.mock_response( + "/v3/packages/package_id/droplets", HTTPStatus.OK, None, "v3", "packages", "GET_{id}_droplets_response.json" + ) + droplets: list[dict] = [droplet for droplet in self.client.v3.packages.list_droplets("package_id")] + self.assertEqual(2, len(droplets)) + self.assertEqual(droplets[0]["state"], "STAGED") diff --git a/tests/v3/test_processes.py b/tests/v3/test_processes.py new file mode 100644 index 0000000..ee501e8 --- /dev/null +++ b/tests/v3/test_processes.py @@ -0,0 +1,43 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestProcesses(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response("/v3/processes", HTTPStatus.OK, None, "v3", "processes", + "GET_response.json") + all_processes = [process for process in self.client.v3.processes.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_processes)) + self.assertEqual(all_processes[0]["type"], "web") + self.assertIsInstance(all_processes[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/processes/process_id", HTTPStatus.OK, None, "v3", "processes", "GET_{id}_response.json" + ) + process = self.client.v3.processes.get("process_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("rackup", process["command"]) + self.assertIsInstance(process, Entity) + + def test_get_then_stats(self): + get_process = self.mock_response("/v3/processes/process_id", HTTPStatus.OK, None, "v3", "processes", + "GET_{id}_response.json") + get_process_stats = self.mock_response("/v3/processes/process_id/stats", HTTPStatus.OK, None, "v3", "processes", + "GET_{id}_stats_response.json") + self.client.get.side_effect = [get_process, get_process_stats] + process = self.client.v3.processes.get("process_id") + stats = [stat for stat in process.stats()] + self.assertEqual(1, len(stats)) + self.assertEqual(stats[0]["state"], "RUNNING") diff --git a/tests/v3/test_roles.py b/tests/v3/test_roles.py new file mode 100644 index 0000000..4be2363 --- /dev/null +++ b/tests/v3/test_roles.py @@ -0,0 +1,37 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestRoles(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response("/v3/roles", HTTPStatus.OK, None, "v3", "roles", + "GET_response.json") + all_roles = [role for role in self.client.v3.roles.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_roles)) + self.assertEqual(all_roles[0]["type"], "organization_auditor") + self.assertIsInstance(all_roles[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/roles/role_id", HTTPStatus.OK, None, "v3", "roles", "GET_{id}_response.json" + ) + role = self.client.v3.roles.get("role_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(role["type"], "organization_auditor") + self.assertIsInstance(role, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/roles/role_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.roles.remove("role_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_routes.py b/tests/v3/test_routes.py new file mode 100644 index 0000000..4bb60f8 --- /dev/null +++ b/tests/v3/test_routes.py @@ -0,0 +1,118 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity +from cloudfoundry_client.v3.routes import LoadBalancing + + +class TestRoutes(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/routes", + HTTPStatus.OK, + None, + "v3", "routes", "POST_response.json" + ) + result = self.client.v3.routes.create( + space_guid="space-guid", + domain_guid="domain-guid", + host="a-hostname", + path="/some_path", + port=6666, + load_balancing=LoadBalancing.ROUND_ROBIN, + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "host": "a-hostname", + "path": "/some_path", + "port": 6666, + "relationships": { + "domain": { + "data": {"guid": "domain-guid"} + }, + "space": { + "data": {"guid": "space-guid"} + } + }, + "options": { + "loadbalancing": "round-robin" + }, + "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/routes", + HTTPStatus.OK, + None, + "v3", "routes", "GET_response.json" + ) + all_routes = [route for route in self.client.v3.routes.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(1, len(all_routes)) + self.assertEqual(all_routes[0]["protocol"], "http") + for route in all_routes: + self.assertIsInstance(route, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/routes/route_id", + HTTPStatus.OK, + None, + "v3", "routes", "GET_{id}_response.json" + ) + result = self.client.v3.routes.get("route_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/routes/route_id", + HTTPStatus.OK, + None, + "v3", "routes", "PATCH_{id}_response.json" + ) + result = self.client.v3.routes.update( + "route_id", + LoadBalancing.LEAST_CONNECTION, + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "options": { + "loadbalancing": "least-connection" + }, + "metadata": { + "labels": {"key": "value"}, + "annotations": {"note": "detailed information"} + } + } + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/routes/route_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.routes.remove("route_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_security_groups.py b/tests/v3/test_security_groups.py new file mode 100644 index 0000000..6670a15 --- /dev/null +++ b/tests/v3/test_security_groups.py @@ -0,0 +1,160 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity, ToManyRelationship, ToOneRelationship +from cloudfoundry_client.v3.security_groups import Rule, RuleProtocol, GloballyEnabled + + +class TestSecurityGroups(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/security_groups", HTTPStatus.CREATED, None, "v3", "security_groups", "POST_response.json" + ) + group_name = "my-group0" + result = self.client.v3.security_groups.create(group_name, rules=[ + Rule(protocol=RuleProtocol.TCP, destination="10.10.10.0/24", ports="443,80,8080"), + Rule(protocol=RuleProtocol.ICMP, destination="10.10.10.0/24", type=8, code=0, + description="Allow ping requests to private services")]) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "name": group_name, + "rules": [ + {"protocol": "tcp", "destination": "10.10.10.0/24", "ports": "443,80,8080"}, + {"protocol": "icmp", "destination": "10.10.10.0/24", "type": 8, "code": 0, + "description": "Allow ping requests to private services"} + ] + }, + files=None + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/security_groups", HTTPStatus.OK, None, "v3", "security_groups", "GET_response.json" + ) + all_security_groups = [security_group for security_group in self.client.v3.security_groups.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_security_groups)) + self.assertEqual(all_security_groups[0]["name"], "my-group0") + self.assertIsInstance(all_security_groups[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/security_groups/security_group_guid_123", HTTPStatus.OK, None, "v3", "security_groups", + "GET_{id}_response.json" + ) + security_group = self.client.v3.security_groups.get("security_group_guid_123") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my-group0", security_group["name"]) + self.assertIsInstance(security_group, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/security_groups/security_group_guid_123", HTTPStatus.OK, None, "v3", "security_groups", + "PATCH_{id}_response.json" + ) + group_name = "my-group0" + result = self.client.v3.security_groups.update("security_group_guid_123", + group_name, + rules=[ + Rule(protocol=RuleProtocol.TCP, destination="10.10.10.0/24", + ports="443,80,8080"), + Rule(protocol=RuleProtocol.ICMP, destination="10.10.10.0/24", + type=8, code=0, + description="Allow ping requests to private services") + ], + globally_enabled=GloballyEnabled(running=True)) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "name": group_name, + "rules": [ + {"protocol": "tcp", "destination": "10.10.10.0/24", "ports": "443,80,8080"}, + {"protocol": "icmp", "destination": "10.10.10.0/24", "type": 8, "code": 0, + "description": "Allow ping requests to private services"} + ], + "globally_enabled": { + "running": True + } + } + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/security_groups/security_group_guid", + HTTPStatus.NO_CONTENT, None) + self.client.v3.security_groups.remove("security_group_guid") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_bind_running_spaces(self): + self.client.post.return_value = self.mock_response( + "/v3/security_groups/security_group_guid/relationships/running_spaces", + HTTPStatus.OK, None, "v3", "security_groups", "POST_{id}_relationships_running_spaces_response.json" + ) + result = self.client.v3.security_groups.bind_running_security_group_to_spaces("security_group_guid", + ToManyRelationship("space-guid1", + "space-guid2")) + + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "data": [ + {"guid": "space-guid1"}, + {"guid": "space-guid2"} + ] + }, + files=None + ) + self.assertIsInstance(result, ToManyRelationship) + self.assertListEqual(["space-guid1", "space-guid2", "previous-space-guid"], result.guids) + + def test_bind_staging_spaces(self): + self.client.post.return_value = self.mock_response( + "/v3/security_groups/security_group_guid/relationships/staging_spaces", + HTTPStatus.OK, None, "v3", "security_groups", "POST_{id}_relationships_staging_spaces_response.json" + ) + result = self.client.v3.security_groups.bind_staging_security_group_to_spaces("security_group_guid", + ToManyRelationship("space-guid1", + "space-guid2")) + + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "data": [ + {"guid": "space-guid1"}, + {"guid": "space-guid2"} + ] + }, + files=None + ) + self.assertIsInstance(result, ToManyRelationship) + self.assertListEqual(["space-guid1", "space-guid2", "previous-space-guid"], result.guids) + + def test_unbind_running_from_space(self): + self.client.delete.return_value = self.mock_response( + "/v3/security_groups/security_group_guid/relationships/running_spaces/space-guid", + HTTPStatus.NO_CONTENT, None) + self.client.v3.security_groups.unbind_running_security_group_from_space("security_group_guid", + ToOneRelationship("space-guid")) + + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_unbind_staging_from_space(self): + self.client.delete.return_value = self.mock_response( + "/v3/security_groups/security_group_guid/relationships/staging_spaces/space-guid", + HTTPStatus.NO_CONTENT, None) + self.client.v3.security_groups.unbind_staging_security_group_from_space("security_group_guid", + ToOneRelationship("space-guid")) + + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_service_brokers.py b/tests/v3/test_service_brokers.py new file mode 100644 index 0000000..fa4ecf3 --- /dev/null +++ b/tests/v3/test_service_brokers.py @@ -0,0 +1,111 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestServiceBrokers(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/service_brokers", HTTPStatus.OK, None, "v3", "service_brokers", "POST_response.json" + ) + username = "us3rn4me" + password = "p4ssw0rd" + url = "https://example.service-broker.com" + result = self.client.v3.service_brokers.create("my_service_broker", url, username, password) + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={ + "name": "my_service_broker", + "url": url, + "authentication": {"type": "basic", "credentials": {"username": username, "password": password}}, + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_create_with_space_guid(self): + self.client.post.return_value = self.mock_response( + "/v3/service_brokers", HTTPStatus.OK, None, "v3", "service_brokers", "POST_response.json" + ) + username = "us3rn4me" + password = "p4ssw0rd" + url = "https://example.service-broker.com" + result = self.client.v3.service_brokers.create("my_service_broker", url, username, password, "space-guid-123") + relationships = {"space": {"data": {"guid": "space-guid-123"}}} + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={ + "name": "my_service_broker", + "url": url, + "authentication": {"type": "basic", "credentials": {"username": username, "password": password}}, + "relationships": relationships, + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/service_brokers", HTTPStatus.OK, None, "v3", "service_brokers", "GET_response.json" + ) + all_service_brokers = [service_broker for service_broker in self.client.v3.service_brokers.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_service_brokers)) + self.assertEqual(all_service_brokers[0]["name"], "my_service_broker") + self.assertIsInstance(all_service_brokers[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/service_brokers/service_broker_guid_123", HTTPStatus.OK, None, "v3", "service_brokers", "GET_{id}_response.json" + ) + service_broker = self.client.v3.service_brokers.get("service_broker_guid_123") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my_service_broker", service_broker["name"]) + self.assertIsInstance(service_broker, Entity) + + def test_update_service_broker_metadata(self): + self.client.patch.return_value = self.mock_response( + "/v3/service_brokers/service_broker_guid_123", + HTTPStatus.OK, + None, + "v3", + "service_brokers", + "PATCH_{id}_response.json", + ) + service_broker = self.client.v3.service_brokers.update(guid="service_broker_guid_123", meta_labels={"hello": "world"}) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={"metadata": {"labels": {"hello": "world"}}}, + ) + self.assertIsNotNone(service_broker) + self.assertIsInstance(service_broker, Entity) + + def test_update_service_broker_name(self): + self.client.patch.return_value = self.mock_response( + "/v3/service_brokers/service_broker_guid_123", + HTTPStatus.OK, + headers={"Location": "http://localhost/v3/jobs/job-guid-123"}, + ) + service_broker = self.client.v3.service_brokers.update(guid="service_broker_guid_123", name="my_service_broker") + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={"name": "my_service_broker"}, + ) + self.assertIsNotNone(service_broker) + self.assertIsInstance(service_broker, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/service_brokers/service_broker_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.service_brokers.remove("service_broker_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_service_credential_bindings.py b/tests/v3/test_service_credential_bindings.py new file mode 100644 index 0000000..945010f --- /dev/null +++ b/tests/v3/test_service_credential_bindings.py @@ -0,0 +1,144 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestCredentialBindings(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create_managed_service_instance(self): + self.client.post.return_value = self.mock_response( + "/v3/service_credential_bindings", HTTPStatus.ACCEPTED, + dict(Location="https://api.example.org/v3/jobs/af5c57f6-8769-41fa-a499-2c84ed896788") + ) + location = self.client.v3.service_credential_bindings.create("some-binding-name", "app", + "7304bc3c-7010-11ea-8840-48bf6bec2d78", + "e0e4417c-74ee-11ea-a604-48bf6bec2d78", + parameters=dict(key1="value1", key2="value2"), + meta_labels=dict(foo="bar"), + meta_annotations=dict(baz="qux")) + + self.assertEqual("af5c57f6-8769-41fa-a499-2c84ed896788", location) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "type": "app", + "name": "some-binding-name", + "relationships": { + "service_instance": { + "data": {"guid": "7304bc3c-7010-11ea-8840-48bf6bec2d78"} + }, + "app": { + "data": {"guid": "e0e4417c-74ee-11ea-a604-48bf6bec2d78"} + } + }, + "parameters": {"key1": "value1", "key2": "value2"}, + "metadata": {"labels": {"foo": "bar"}, "annotations": {"baz": "qux"}}, + }, + ) + + def test_create_user_provided_service_instance(self): + self.client.post.return_value = self.mock_response( + "/v3/service_credential_bindings", HTTPStatus.ACCEPTED, + None, + "v3", "service_credential_bindings", "POST_response.json" + ) + result = self.client.v3.service_credential_bindings.create("some-binding-name", "key", + "7304bc3c-7010-11ea-8840-48bf6bec2d78", + None, + parameters=dict(key1="value1", key2="value2"), + meta_labels=dict(foo="bar"), + meta_annotations=dict(baz="qux")) + self.assertIsNotNone(result) + self.assertEqual("some-name", result["name"]) + self.assertIsInstance(result, Entity) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "type": "key", + "name": "some-binding-name", + "relationships": { + "service_instance": { + "data": {"guid": "7304bc3c-7010-11ea-8840-48bf6bec2d78"} + } + }, + "parameters": {"key1": "value1", "key2": "value2"}, + "metadata": {"labels": {"foo": "bar"}, "annotations": {"baz": "qux"}}, + }, + ) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/service_credential_bindings", HTTPStatus.OK, None, "v3", "service_credential_bindings", + "GET_response.json" + ) + all_service_credential_bindings = [credential_binding for credential_binding in + self.client.v3.service_credential_bindings.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_service_credential_bindings)) + self.assertEqual(all_service_credential_bindings[0]["name"], "some-binding-name") + for domain in all_service_credential_bindings: + self.assertIsInstance(domain, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/service_credential_bindings/service_credential_binding_id", + HTTPStatus.OK, + None, + "v3", + "service_credential_bindings", "GET_{id}_response.json" + ) + result = self.client.v3.service_credential_bindings.get("service_credential_binding_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_get_then_details(self): + get_service_credential_binding = self.mock_response( + "/v3/service_credential_bindings/service_credential_binding_id", + HTTPStatus.OK, + None, + "v3", + "service_credential_bindings", + "GET_{id}_response.json") + get_details = self.mock_response( + "/v3/service_credential_bindings/service_credential_binding_id/details", + HTTPStatus.OK, + None, + "v3", + "service_credential_bindings", + "GET_{id}_details_response.json", + ) + self.client.get.side_effect = [get_service_credential_binding, get_details] + service_credential_binding = self.client.v3.service_credential_bindings.get("service_credential_binding_id") + details = service_credential_binding.details() + self.assertIsInstance(details, dict) + self.assertEqual("mydb://user@password:example.com", details["credentials"]["connection"]) + + def test_get_then_parameters(self): + get_service_credential_binding = self.mock_response( + "/v3/service_credential_bindings/service_credential_binding_id", + HTTPStatus.OK, + None, "v3", + "service_credential_bindings", + "GET_{id}_response.json") + get_parameters = self.mock_response( + "/v3/service_credential_bindings/service_credential_binding_id/parameters", + HTTPStatus.OK, + None, + "v3", + "service_credential_bindings", + "GET_{id}_parameters_response.json", + ) + self.client.get.side_effect = [get_service_credential_binding, get_parameters] + service_credential_binding = self.client.v3.service_credential_bindings.get("service_credential_binding_id") + parameters = service_credential_binding.parameters() + self.assertIsInstance(parameters, dict) + self.assertEqual("bar", parameters["foo"]) diff --git a/tests/v3/test_service_instances.py b/tests/v3/test_service_instances.py new file mode 100644 index 0000000..f461e5b --- /dev/null +++ b/tests/v3/test_service_instances.py @@ -0,0 +1,225 @@ +import unittest +from http import HTTPStatus +from unittest.mock import patch + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.common_objects import JsonObject +from cloudfoundry_client.v3.entities import Entity + + +class TestServiceInstances(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/service_instances", + HTTPStatus.ACCEPTED, + headers={"Location": "https://somewhere.org/v3/jobs/job_id"}, + ) + result = self.client.v3.service_instances.create( + name="space-name", + parameters={"foo": "bar"}, + tags=["mytag", "myothertag"], + space_guid="space-guid-123", + service_plan_guid="service-plan-guid-123", + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "name": "space-name", + "parameters": {"foo": "bar"}, + "tags": ["mytag", "myothertag"], + "type": "managed", + "relationships": { + "space": {"data": {"guid": "space-guid-123"}}, + "service_plan": {"data": {"guid": "service-plan-guid-123"}}, + }, + }, + files=None, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/service_instances/instance-guid-123", + HTTPStatus.ACCEPTED, + headers={"Location": "https://somewhere.org/v3/jobs/job_id"}, + ) + result = self.client.v3.service_instances.update( + instance_guid="instance-guid-123", + name="space-name", + parameters={"foo": "bar"}, + service_plan="custom_service_plan", + maintenance_info="1.2.3", + meta_labels={"foo": "bar"}, + meta_annotations={"foo": "bar"}, + tags=["mytag", "myothertag"], + ) + + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "name": "space-name", + "parameters": {"foo": "bar"}, + "relationships": { + "service_plan": { + "data": {"guid": "custom_service_plan"}}, + }, + "maintenance_info": {"version": "1.2.3"}, + "tags": ["mytag", "myothertag"], + "metadata": {'labels': {'foo': 'bar'}, + 'annotations': {'foo': 'bar'}} + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/service_instances", HTTPStatus.OK, None, "v3", "service_instances", "GET_response.json" + ) + all_service_instances = [service_instance for service_instance in self.client.v3.service_instances.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(1, len(all_service_instances)) + self.assertEqual(all_service_instances[0]["guid"], "88ce23e5-27c3-4381-a2df-32a28ec43133") + self.assertIsInstance(all_service_instances[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/service_instances/service_instance_id", HTTPStatus.OK, None, "v3", "service_instances", "GET_{id}_response.json" + ) + service_instance = self.client.v3.service_instances.get("service_instance_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + 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") + get_credentials = self.mock_response( + "/v3/service_credential_bindings/service_instance_id/credentials", + HTTPStatus.OK, + None, + "v3", + "service_instances", + "GET_{id}_credentials_response.json", + ) + self.client.get.side_effect = [get_service_instance, get_credentials] + service_instance = self.client.v3.service_instances.get("service_instance_id") + credentials = service_instance.credentials() + self.assertIsInstance(credentials, dict) + self.assertEqual("super-secret", credentials["password"]) + + def test_remove_user_provided_service_instance(self): + self.client.delete.return_value = self.mock_response( + "/v3/service_instances/service_instance_id", HTTPStatus.NO_CONTENT, None + ) + self.client.v3.service_instances.remove("service_instance_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_remove(self): + self.client.delete.return_value = self.mock_response( + "/v3/service_instances/service_instance_id", + HTTPStatus.ACCEPTED, + headers={"Location": "https://somewhere.org/v3/jobs/job_id"}, + ) + self.client.v3.service_instances.remove("service_instance_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + @patch("time.sleep", return_value=None) + def test_remove_synchronous(self, sleepmock): + self.client.delete.return_value = self.mock_response( + "/v3/service_instances/service_instance_id", HTTPStatus.ACCEPTED, {"Location": "https://somewhere.org/v3/jobs/job_id"} + ) + self.client.get.side_effect = [ + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_processing_response.json", + ), + self.mock_response( + "/v3/jobs/job_id", + HTTPStatus.OK, + None, + "v3", + "jobs", + "GET_{id}_complete_response.json", + ), + ] + self.client.v3.service_instances.remove("service_instance_id", asynchronous=False) + self.client.delete.assert_called_with(self.client.delete.return_value.url) + assert self.client.get.call_count == 2 + + def test_get_permissions(self): + self.client.get.return_value = self.mock_response( + "/v3/service_instances/service_instance_id/permissions", + HTTPStatus.OK, + None, + "v3", + "service_instances", + "GET_{id}_permissions_response.json" + ) + permissions = self.client.v3.service_instances.get_permissions("service_instance_id") + self.assertIsInstance(permissions, JsonObject) + self.assertTrue(permissions["read"]) + self.assertFalse(permissions["manage"]) diff --git a/tests/v3/test_service_offerings.py b/tests/v3/test_service_offerings.py new file mode 100644 index 0000000..a763651 --- /dev/null +++ b/tests/v3/test_service_offerings.py @@ -0,0 +1,64 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestServiceOfferings(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/service_offerings/service_offering_guid", + HTTPStatus.OK, + None, + "v3", + "service_offerings", + "GET_{id}_response.json", + ) + service_offering = self.client.v3.service_offerings.get("service_offering_guid") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my_service_offering", service_offering["name"]) + self.assertIsInstance(service_offering, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/service_offerings", HTTPStatus.OK, None, "v3", "service_offerings", "GET_response.json" + ) + all_service_offerings = [service_offerings for service_offerings in self.client.v3.service_offerings.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_service_offerings)) + self.assertEqual(all_service_offerings[0]["name"], "my_service_offering") + self.assertIsInstance(all_service_offerings[0], Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/service_offerings/service_offering_guid", + HTTPStatus.OK, + None, + "v3", + "service_offerings", + "PATCH_{id}_response.json", + ) + service_offering = self.client.v3.service_offerings.update(guid="service_offering_guid", meta_labels={"hello": "world"}) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={"metadata": {"labels": {"hello": "world"}}}, + ) + self.assertIsNotNone(service_offering) + self.assertIsInstance(service_offering, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response( + "/v3/service_offerings/service_offering_guid", HTTPStatus.NO_CONTENT, None + ) + self.client.v3.service_offerings.remove(guid="service_offering_guid", purge=True) + self.client.delete.assert_called_with( + f"{self.client.delete.return_value.url}?purge=true", + ) diff --git a/tests/v3/test_service_plans.py b/tests/v3/test_service_plans.py new file mode 100644 index 0000000..7204fb2 --- /dev/null +++ b/tests/v3/test_service_plans.py @@ -0,0 +1,127 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestServicePlans(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/service_plans/service_plan_guid_123", HTTPStatus.OK, None, "v3", "service_plans", "GET_{id}_response.json" + ) + service_plan = self.client.v3.service_plans.get("service_plan_guid_123") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my_service_plan", service_plan["name"]) + self.assertIsInstance(service_plan, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/service_plans", HTTPStatus.OK, None, "v3", "service_plans", "GET_response.json" + ) + all_service_plans = [service_plan for service_plan in self.client.v3.service_plans.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_service_plans)) + self.assertEqual(all_service_plans[0]["name"], "my_big_service_plan") + self.assertIsInstance(all_service_plans[0], Entity) + + def test_update_service_plan(self): + self.client.patch.return_value = self.mock_response( + "/v3/service_plans/service_plan_guid_123", HTTPStatus.OK, None, "v3", "service_plans", "PATCH_{id}_response.json" + ) + service_plan = self.client.v3.service_plans.update( + guid="service_plan_guid_123", meta_labels={"hello": "world"}, meta_annotations={"note": "detailed information"} + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={"metadata": {"labels": {"hello": "world"}, "annotations": {"note": "detailed information"}}}, + ) + self.assertIsNotNone(service_plan) + self.assertIsInstance(service_plan, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/service_plans/service_plan_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.service_plans.remove("service_plan_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_get_service_plan_visibility(self): + self.client.get.return_value = self.mock_response( + "/v3/service_plans/service_plan_guid_123/visibility", + HTTPStatus.NO_CONTENT, + None, + "v3", + "service_plans", + "GET_{id}_visibility_response.json", + ) + service_plan_visibility = self.client.v3.service_plans.get_visibility(service_plan_guid="service_plan_guid_123") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("public", service_plan_visibility["type"]) + + def test_update_service_plan_visibility_type(self): + self.client.patch.return_value = self.mock_response( + "/v3/service_plans/service_plan_guid_123/visibility", + HTTPStatus.NO_CONTENT, + None, + "v3", + "service_plans", + "PATCH_{id}_visibility_type_response.json", + ) + service_plan_visibility = self.client.v3.service_plans.update_visibility( + service_plan_guid="service_plan_guid_123", type="admin" + ) + self.client.patch.assert_called_with(self.client.patch.return_value.url, json={"type": "admin"}) + self.assertEqual("admin", service_plan_visibility["type"]) + + def test_update_service_plan_visibility_organizations(self): + self.client.patch.return_value = self.mock_response( + "/v3/service_plans/service_plan_guid_123/visibility", + HTTPStatus.NO_CONTENT, + None, + "v3", + "service_plans", + "PATCH_{id}_visibility_organizations_response.json", + ) + organizations = [{"guid": "0fc1ad4f-e1d7-4436-8e23-6b20f03c6482"}, {"guid": "0fc1ad4f-e1d7-4436-8e23-6b20f03c6483"}] + service_plan_visibility = self.client.v3.service_plans.update_visibility( + service_plan_guid="service_plan_guid_123", type="organization", organizations=organizations + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, json={"type": "organization", "organizations": organizations} + ) + self.assertEqual("organization", service_plan_visibility["type"]) + self.assertEqual("some_org", service_plan_visibility["organizations"][0]["name"]) + + def test_apply_visibility_to_extra_orgs(self): + self.client.post.return_value = self.mock_response( + "/v3/service_plans/service_plan_guid_123/visibility", + HTTPStatus.NO_CONTENT, + None, + "v3", + "service_plans", + "POST_{id}_visibility_response.json", + ) + organizations = [{"guid": "b3af3658-d844-496a-8986-89b79a74c8ae"}] + service_plan_visibility = self.client.v3.service_plans.apply_visibility_to_extra_orgs( + service_plan_guid="service_plan_guid_123", organizations=organizations + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, json={"type": "organization", "organizations": organizations}, files=None + ) + self.assertEqual("organization", service_plan_visibility["type"]) + self.assertEqual("b3af3658-d844-496a-8986-89b79a74c8ae", service_plan_visibility["organizations"][0]["guid"]) + + def test_remove_org_from_service_plan_visibility(self): + self.client.delete.return_value = self.mock_response( + "/v3/service_plans/service_plan_guid_123/visibility/org_guid_123", HTTPStatus.NO_CONTENT, None + ) + self.client.v3.service_plans.remove_org_from_service_plan_visibility( + service_plan_guid="service_plan_guid_123", org_guid="org_guid_123" + ) + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_spaces.py b/tests/v3/test_spaces.py new file mode 100644 index 0000000..5963200 --- /dev/null +++ b/tests/v3/test_spaces.py @@ -0,0 +1,124 @@ +import unittest +from http import HTTPStatus +from unittest.mock import call + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity, ToOneRelationship + + +class TestSpaces(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/spaces", HTTPStatus.OK, None, "v3", "spaces", "POST_response.json" + ) + result = self.client.v3.spaces.create("space-name", "organization-guid") + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={"name": "space-name", "relationships": {"organization": {"data": {"guid": "organization-guid"}}}}, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/spaces", HTTPStatus.OK, None, "v3", "spaces", "GET_response.json" + ) + all_spaces = [space for space in self.client.v3.spaces.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_spaces)) + self.assertEqual(all_spaces[0]["name"], "space1") + self.assertIsInstance(all_spaces[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/spaces/space_id", HTTPStatus.OK, None, "v3", "spaces", "GET_{id}_response.json" + ) + space = self.client.v3.spaces.get("space_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my-space", space["name"]) + self.assertIsInstance(space, Entity) + + def test_get_then_organization(self): + get_space = self.mock_response( + "/v3/spaces/space_id", HTTPStatus.OK, None, "v3", "spaces", "GET_{id}_response.json" + ) + get_organization = self.mock_response( + "/v3/organizations/e00705b9-7b42-4561-ae97-2520399d2133", + HTTPStatus.OK, + None, + "v3", + "organizations", + "GET_{id}_response.json", + ) + self.client.get.side_effect = [get_space, get_organization] + organization = self.client.v3.spaces.get("space_id").organization() + self.client.get.assert_has_calls([call(get_space.url), call(get_organization.url)], any_order=False) + self.assertEqual("my-organization", organization["name"]) + + def test_get_assigned_isolation_segment(self): + self.client.get.return_value = self.mock_response( + "/v3/spaces/space_id/relationships/isolation_segment", + HTTPStatus.OK, + None, + "v3", + "spaces", + "GET_{id}_relationships_isolation_segment_response.json", + ) + + result = self.client.v3.spaces.get_assigned_isolation_segment("space_id") + + self.assertIsInstance(result, ToOneRelationship) + self.assertEqual("e4c91047-3b29-4fda-b7f9-04033e5a9c9f", result.guid) + + def test_assign_isolation_segment(self): + self.client.patch.return_value = self.mock_response( + "/v3/spaces/space_id/relationships/isolation_segment", + HTTPStatus.OK, + None, + "v3", + "spaces", + "POST_{id}_relationships_isolation_segment_response.json", + ) + result = self.client.v3.spaces.assign_isolation_segment("space_id", "iso-seg-guid") + self.client.patch.assert_called_with(self.client.patch.return_value.url, json={"data": {"guid": "iso-seg-guid"}}) + self.assertIsInstance(result, ToOneRelationship) + self.assertEqual("iso-seg-guid", result.guid) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/spaces/space_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.spaces.remove("space_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) + + def test_get_include_organization(self): + self.client.get.return_value = self.mock_response( + "/v3/spaces/space_id?include=organization", + HTTPStatus.OK, + None, + "v3", + "spaces", + "GET_{id}_response_include_org.json" + ) + org = self.client.v3.spaces.get("space_id", include="organization").organization() + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my-organization", org["name"]) + self.assertIsInstance(org, Entity) + + def test_list_include_organization(self): + self.client.get.return_value = self.mock_response( + "/v3/spaces?include=organization", HTTPStatus.OK, None, "v3", "spaces", "GET_response_include_org.json" + ) + all_orgs = [space.organization() for space in self.client.v3.spaces.list(include="organization")] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_orgs)) + self.assertEqual(all_orgs[0]["name"], "org1") + self.assertIsInstance(all_orgs[0], Entity) + self.assertEqual(all_orgs[1]["name"], "org2") + self.assertIsInstance(all_orgs[1], Entity) diff --git a/tests/v3/test_stacks.py b/tests/v3/test_stacks.py new file mode 100644 index 0000000..a5cd581 --- /dev/null +++ b/tests/v3/test_stacks.py @@ -0,0 +1,88 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestStacks(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/stacks", HTTPStatus.CREATED, None, "v3", "stacks", "POST_response.json" + ) + result = self.client.v3.stacks.create("my-stack", "Here is my stack!") + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "name": "my-stack", + "description": "Here is my stack!", + }, + files=None + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/stacks", HTTPStatus.OK, None, "v3", "stacks", "GET_response.json" + ) + all_stacks = [stack for stack in self.client.v3.stacks.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_stacks)) + self.assertEqual(all_stacks[0]["name"], "my-stack-1") + self.assertIsInstance(all_stacks[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/stacks/stack-id", HTTPStatus.OK, None, "v3", "stacks", + "GET_{id}_response.json" + ) + stack = self.client.v3.stacks.get("stack-id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my-stack", stack["name"]) + self.assertIsInstance(stack, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/stacks/stack-id", HTTPStatus.OK, None, + "v3", "stacks", "PATCH_{id}_response.json" + ) + result = self.client.v3.stacks.update("stack-id", + {"key": "value"}, + {"note": "detailed information"} + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "metadata": { + "labels": {"key": "value"}, + "annotations": {"note": "detailed information"} + } + } + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list_apps(self): + self.client.get.return_value = self.mock_response( + "/v3/stacks/stack-id/apps", HTTPStatus.OK, None, + "v3", "stacks", "GET_{id}_apps_response.json" + ) + all_apps = [app for app in self.client.v3.stacks.list_apps('stack-id')] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_apps)) + self.assertEqual(all_apps[0]["name"], "my_app") + self.assertIsInstance(all_apps[0], Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/stacks/stack-id", + HTTPStatus.NO_CONTENT, None) + self.client.v3.stacks.remove("stack-id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_tasks.py b/tests/v3/test_tasks.py new file mode 100644 index 0000000..8f1a0cc --- /dev/null +++ b/tests/v3/test_tasks.py @@ -0,0 +1,89 @@ +import sys +import unittest +from http import HTTPStatus +from unittest.mock import patch + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.main import main +from cloudfoundry_client.v3.entities import Entity + + +class TestTasks(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response("/v3/tasks", HTTPStatus.OK, None, "v3", "tasks", "GET_response.json") + all_tasks = [task for task in self.client.v3.tasks.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_tasks)) + self.assertEqual(all_tasks[0]["name"], "hello") + self.assertIsInstance(all_tasks[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/tasks/task_id", HTTPStatus.OK, None, "v3", "tasks", "GET_{id}_response.json" + ) + task = self.client.v3.tasks.get("task_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("migrate", task["name"]) + self.assertIsInstance(task, Entity) + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/apps/app_guid/tasks", HTTPStatus.CREATED, None, "v3", "tasks", "POST_response.json" + ) + task = self.client.v3.tasks.create("app_guid", command="rake db:migrate") + self.client.post.assert_called_with(self.client.post.return_value.url, files=None, json=dict(command="rake db:migrate")) + self.assertIsNotNone(task) + + def test_cancel(self): + self.client.post.return_value = self.mock_response( + "/v3/tasks/task_guid/actions/cancel", + HTTPStatus.ACCEPTED, + None, + "v3", + "tasks", + "POST_{id}_actions_cancel_response.json", + ) + task = self.client.v3.tasks.cancel("task_guid") + self.client.post.assert_called_with(self.client.post.return_value.url, files=None, json=None) + self.assertIsNotNone(task) + + @patch.object(sys, "argv", ["main", "list_tasks", "-names", "task_name"]) + def test_list_tasks(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.get.return_value = self.mock_response( + "/v3/tasks?names=task_name", HTTPStatus.OK, None, "v3", "tasks", "GET_response.json" + ) + main.main() + self.client.get.assert_called_with(self.client.get.return_value.url) + + @patch.object(sys, "argv", ["main", "create_task", "app_id", '{"command": "rake db:migrate", "name": "example"}']) + def test_create_task(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.post.return_value = self.mock_response( + "/v3/apps/app_id/tasks", HTTPStatus.CREATED, None, "v3", "tasks", "POST_response.json" + ) + main.main() + self.client.post.assert_called_with( + self.client.post.return_value.url, files=None, json=dict(command="rake db:migrate", name="example") + ) + + @patch.object(sys, "argv", ["main", "cancel_task", "task_id"]) + def test_cancel_task(self): + with patch("cloudfoundry_client.main.main.build_client_from_configuration", new=lambda: self.client): + self.client.post.return_value = self.mock_response( + "/v3/tasks/task_id/actions/cancel", + HTTPStatus.CREATED, + None, + "v3", + "tasks", + "POST_{id}_actions_cancel_response.json", + ) + main.main() + self.client.post.assert_called_with(self.client.post.return_value.url, files=None, json=None) diff --git a/tests/v3/test_users.py b/tests/v3/test_users.py new file mode 100644 index 0000000..a295e1e --- /dev/null +++ b/tests/v3/test_users.py @@ -0,0 +1,73 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestUsers(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/users", HTTPStatus.OK, None, "v3", "users", "POST_response.json" + ) + result = self.client.v3.users.create("3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={"guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5"}, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/users", HTTPStatus.OK, None, "v3", "users", "GET_response.json" + ) + all_users = [user for user in self.client.v3.users.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_users)) + self.assertEqual(all_users[0]["guid"], "client_id") + self.assertIsInstance(all_users[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/users/user_id", HTTPStatus.OK, None, "v3", "users", "GET_{id}_response.json" + ) + user = self.client.v3.users.get("user_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("uaa", user["origin"]) + self.assertIsInstance(user, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/users/user_id", HTTPStatus.OK, None, "v3", "users", "PATCH_{id}_response.json" + ) + result = self.client.v3.users.update( + "user_id", + {"environment": "production"}, + {"note": "detailed information"} + ) + + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "metadata": { + "labels": {"environment": "production"}, + "annotations": {"note": "detailed information"} + } + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/users/user_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.users.remove("user_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/vendors/dropsonde-protocol/envelope.proto b/vendors/dropsonde-protocol/envelope.proto index 024ee38..1a978ba 100644 --- a/vendors/dropsonde-protocol/envelope.proto +++ b/vendors/dropsonde-protocol/envelope.proto @@ -1,4 +1,9 @@ +syntax = "proto2"; + package events; + +option go_package = "github.com/cloudfoundry/sonde-go/events"; + option java_package = "org.cloudfoundry.dropsonde.events"; option java_outer_classname = "EventFactory"; @@ -7,13 +12,12 @@ import "log.proto"; import "metric.proto"; import "error.proto"; -/// Envelope wraps an Event and adds metadata. +// Envelope wraps an Event and adds metadata. message Envelope { - /// Type of the wrapped event. + // Type of the wrapped event. enum EventType { - // Removed Heartbeat at position 1 - // Removed HttpStart at position 2 - // Removed HttpStop at position 3 + reserved 1 to 3; + reserved "Heartbeat", "HttpStart", "HttpStop"; HttpStartStop = 4; LogMessage = 5; ValueMetric = 6; @@ -22,21 +26,34 @@ message Envelope { ContainerMetric = 9; } - required string origin = 1; /// Unique description of the origin of this event. - required EventType eventType = 2; /// Type of wrapped event. Only the optional field corresponding to the value of eventType should be set. + // Unique description of the origin of this event. + required string origin = 1; + + // Type of wrapped event. Only the optional field corresponding to the + // value of eventType should be set. + required EventType eventType = 2; + + // UNIX timestamp (in nanoseconds) event was wrapped in this Envelope. + optional int64 timestamp = 6; - optional int64 timestamp = 6; /// UNIX timestamp (in nanoseconds) event was wrapped in this Envelope. + // Deployment name (used to uniquely identify source). + optional string deployment = 13; - optional string deployment = 13; /// Deployment name (used to uniquely identify source). - optional string job = 14; /// Job name (used to uniquely identify source). - optional string index = 15; /// Index of job (used to uniquely identify source). - optional string ip = 16; /// IP address (used to uniquely identify source). + // Job name (used to uniquely identify source). + optional string job = 14; - map tags = 17; /// key/value tags to include additional identifying information. + // Index of job (used to uniquely identify source). + optional string index = 15; + + // IP address (used to uniquely identify source). + optional string ip = 16; + + // key/value tags to include additional identifying information. + map tags = 17; + + reserved 3 to 5; + reserved "Heartbeat", "HttpStart", "HttpStop"; - // Removed Heartbeat at position 3 - // Removed HttpStart at position 4 - // Removed HttpStop at position 5 optional HttpStartStop httpStartStop = 7; optional LogMessage logMessage = 8; optional ValueMetric valueMetric = 9; @@ -44,4 +61,3 @@ message Envelope { optional Error error = 11; optional ContainerMetric containerMetric = 12; } - diff --git a/vendors/dropsonde-protocol/error.proto b/vendors/dropsonde-protocol/error.proto index 9ba2baf..9e64db3 100644 --- a/vendors/dropsonde-protocol/error.proto +++ b/vendors/dropsonde-protocol/error.proto @@ -1,11 +1,22 @@ +syntax = "proto2"; + package events; +option go_package = "github.com/cloudfoundry/sonde-go/events"; + option java_package = "org.cloudfoundry.dropsonde.events"; option java_outer_classname = "ErrorFactory"; -/// An Error event represents an error in the originating process. +// An Error event represents an error in the originating process. message Error { - required string source = 1; /// Source of the error. This may or may not be the same as the Origin in the envelope. - required int32 code = 2; /// Numeric error code. This is provided for programmatic responses to the error. - required string message = 3; /// Error description (preferably human-readable). + // Source of the error. This may or may not be the same as the Origin in + // the envelope. + required string source = 1; + + // Numeric error code. This is provided for programmatic responses to the + // error. + required int32 code = 2; + + // Error description (preferably human-readable). + required string message = 3; } diff --git a/vendors/dropsonde-protocol/http.proto b/vendors/dropsonde-protocol/http.proto index 2e9c9b4..2ec17d7 100644 --- a/vendors/dropsonde-protocol/http.proto +++ b/vendors/dropsonde-protocol/http.proto @@ -1,17 +1,24 @@ +syntax = "proto2"; + package events; +option go_package = "github.com/cloudfoundry/sonde-go/events"; + option java_package = "org.cloudfoundry.dropsonde.events"; option java_outer_classname = "HttpFactory"; import "uuid.proto"; -/// Type of peer handling request. +// Type of peer handling request. enum PeerType { - Client = 1; /// Request is made by this process. - Server = 2; /// Request is received by this process. + // Request is made by this process. + Client = 1; + + // Request is received by this process. + Server = 2; } -/// HTTP method. +// HTTP method. enum Method { GET = 1; POST = 2; @@ -60,25 +67,54 @@ enum Method { VERSION_CONTROL = 44; } -/// An HttpStartStop event represents the whole lifecycle of an HTTP request. +// An HttpStartStop event represents the whole lifecycle of an HTTP request. message HttpStartStop { - required int64 startTimestamp = 1; /// UNIX timestamp (in nanoseconds) when the request was sent (by a client) or received (by a server). - required int64 stopTimestamp = 2; /// UNIX timestamp (in nanoseconds) when the request was received. + // UNIX timestamp (in nanoseconds) when the request was sent (by a client) + // or received (by a server). + required int64 startTimestamp = 1; + + // UNIX timestamp (in nanoseconds) when the request was received. + required int64 stopTimestamp = 2; + + // ID for tracking lifecycle of request. + required UUID requestId = 3; + + // Role of the emitting process in the request cycle. + required PeerType peerType = 4; + + // Method of the request. + required Method method = 5; + + // Destination of the request. + required string uri = 6; + + // Remote address of the request. (For a server, this should be the origin + // of the request.) + required string remoteAddress = 7; + + // Contents of the UserAgent header on the request. + required string userAgent = 8; + + // Status code returned with the response to the request. + required int32 statusCode = 9; + + // Length of response (bytes). + required int64 contentLength = 10; + + reserved 11; + reserved "parentRequestId"; - required UUID requestId = 3; /// ID for tracking lifecycle of request. - required PeerType peerType = 4; /// Role of the emitting process in the request cycle. - required Method method = 5; /// Method of the request. - required string uri = 6; /// Destination of the request. - required string remoteAddress = 7; /// Remote address of the request. (For a server, this should be the origin of the request.) - required string userAgent = 8; /// Contents of the UserAgent header on the request. + // If this request was made in relation to an appliciation, this field + // should track that application's ID. + optional UUID applicationId = 12; - required int32 statusCode = 9; /// Status code returned with the response to the request. - required int64 contentLength = 10; /// Length of response (bytes). + // Index of the application instance. + optional int32 instanceIndex = 13; - /// 11 used to be ParentRequestID which has been deprecated. + // ID of the application instance. + optional string instanceId = 14; - optional UUID applicationId = 12; /// If this request was made in relation to an appliciation, this field should track that application's ID. - optional int32 instanceIndex = 13; /// Index of the application instance. - optional string instanceId = 14; /// ID of the application instance. - repeated string forwarded = 15; /// This contains http forwarded-for [x-forwarded-for] header from the request. + // This contains http forwarded-for [x-forwarded-for] header from the + // request. + repeated string forwarded = 15; } diff --git a/vendors/dropsonde-protocol/log.proto b/vendors/dropsonde-protocol/log.proto index c012f87..2174a2e 100644 --- a/vendors/dropsonde-protocol/log.proto +++ b/vendors/dropsonde-protocol/log.proto @@ -1,21 +1,35 @@ +syntax = "proto2"; + package events; +option go_package = "github.com/cloudfoundry/sonde-go/events"; + option java_package = "org.cloudfoundry.dropsonde.events"; option java_outer_classname = "LogFactory"; -/// A LogMessage contains a "log line" and associated metadata. +// A LogMessage contains a "log line" and associated metadata. message LogMessage { - - /// MessageType stores the destination of the message (corresponding to STDOUT or STDERR). + // MessageType stores the destination of the message (corresponding to STDOUT or STDERR). enum MessageType { OUT = 1; ERR = 2; } - required bytes message = 1; /// Bytes of the log message. (Note that it is not required to be a single line.) - required MessageType message_type = 2; /// Type of the message (OUT or ERR). - required int64 timestamp = 3; /// UNIX timestamp (in nanoseconds) when the log was written. - optional string app_id = 4; /// Application that emitted the message (or to which the application is related). - optional string source_type = 5; /// Source of the message. For Cloud Foundry, this can be "APP", "RTR", "DEA", "STG", etc. - optional string source_instance = 6; /// Instance that emitted the message. + // Bytes of the log message. (Note that it is not required to be a single line.) + required bytes message = 1; + + // Type of the message (OUT or ERR). + required MessageType message_type = 2; + + // UNIX timestamp (in nanoseconds) when the log was written. + required int64 timestamp = 3; + + // Application that emitted the message (or to which the application is related). + optional string app_id = 4; + + // Source of the message. For Cloud Foundry, this can be "APP", "RTR", "DEA", "STG", etc. + optional string source_type = 5; + + // Instance that emitted the message. + optional string source_instance = 6; } diff --git a/vendors/dropsonde-protocol/metric.proto b/vendors/dropsonde-protocol/metric.proto index 006344d..d6fead9 100644 --- a/vendors/dropsonde-protocol/metric.proto +++ b/vendors/dropsonde-protocol/metric.proto @@ -1,32 +1,65 @@ +syntax = "proto2"; + package events; +option go_package = "github.com/cloudfoundry/sonde-go/events"; + option java_package = "org.cloudfoundry.dropsonde.events"; option java_outer_classname = "MetricFactory"; -import "uuid.proto"; - -/// A ValueMetric indicates the value of a metric at an instant in time. +// A ValueMetric indicates the value of a metric at an instant in time. message ValueMetric { - required string name = 1; /// Name of the metric. Must be consistent for downstream consumers to associate events semantically. - required double value = 2; /// Value at the time of event emission. - required string unit = 3; /// Unit of the metric. Please see http://metrics20.org/spec/#units for ideas; SI units/prefixes are recommended where applicable. Should be consistent for the life of the metric (consumers are expected to report, but not interpret, prefixes). + // Name of the metric. Must be consistent for downstream consumers to + // associate events semantically. + required string name = 1; + + // Value at the time of event emission. + required double value = 2; + + // Unit of the metric. Please see http://metrics20.org/spec/#units for + // ideas; SI units/prefixes are recommended where applicable. Should be + // consistent for the life of the metric (consumers are expected to report, + // but not interpret, prefixes). + required string unit = 3; } -/// A CounterEvent represents the increment of a counter. It contains only the change in the value; it is the responsibility of downstream consumers to maintain the value of the counter. +// A CounterEvent represents the increment of a counter. It contains only the +// change in the value; it is the responsibility of downstream consumers to +// maintain the value of the counter. message CounterEvent { - required string name = 1; /// Name of the counter. Must be consistent for downstream consumers to associate events semantically. - required uint64 delta = 2; /// Amount by which to increment the counter. - optional uint64 total = 3; /// Total value of the counter. This will be overridden by Metron, which internally tracks the total of each named Counter it receives. + // Name of the counter. Must be consistent for downstream consumers to + // associate events semantically. + required string name = 1; + + // Amount by which to increment the counter. + required uint64 delta = 2; + + // Total value of the counter. This will be overridden by Metron, which + // internally tracks the total of each named Counter it receives. + optional uint64 total = 3; } -/// A ContainerMetric records resource usage of an app in a container. +// A ContainerMetric records resource usage of an app in a container. message ContainerMetric { - required string applicationId = 1; /// ID of the contained application. - required int32 instanceIndex = 2; /// Instance index of the contained application. (This, with applicationId, should uniquely identify a container.) - - required double cpuPercentage = 3; /// CPU based on number of cores. - required uint64 memoryBytes = 4; /// Bytes of memory used. - required uint64 diskBytes = 5; /// Bytes of disk used. - optional uint64 memoryBytesQuota = 6; /// Maximum bytes of memory allocated to container. - optional uint64 diskBytesQuota = 7; /// Maximum bytes of disk allocated to container. + // ID of the contained application. + required string applicationId = 1; + + // Instance index of the contained application. (This, with applicationId, + // should uniquely identify a container.) + required int32 instanceIndex = 2; + + // CPU based on number of cores. + required double cpuPercentage = 3; + + // Bytes of memory used. + required uint64 memoryBytes = 4; + + // Bytes of disk used. + required uint64 diskBytes = 5; + + // Maximum bytes of memory allocated to container. + optional uint64 memoryBytesQuota = 6; + + // Maximum bytes of disk allocated to container. + optional uint64 diskBytesQuota = 7; } diff --git a/vendors/dropsonde-protocol/uuid.proto b/vendors/dropsonde-protocol/uuid.proto index 44c1c5a..686155d 100644 --- a/vendors/dropsonde-protocol/uuid.proto +++ b/vendors/dropsonde-protocol/uuid.proto @@ -1,11 +1,17 @@ +syntax = "proto2"; + package events; +option go_package = "github.com/cloudfoundry/sonde-go/events"; + option java_package = "org.cloudfoundry.dropsonde.events"; option java_outer_classname = "UuidFactory"; -/// Type representing a 128-bit UUID. +// Type representing a 128-bit UUID. // -// The bytes of the UUID should be packed in little-endian **byte** (not bit) order. For example, the UUID `f47ac10b-58cc-4372-a567-0e02b2c3d479` should be encoded as `UUID{ low: 0x7243cc580bc17af4, high: 0x79d4c3b2020e67a5 }` +// The bytes of the UUID should be packed in little-endian **byte** (not bit) +// order. For example, the UUID `f47ac10b-58cc-4372-a567-0e02b2c3d479` should +// be encoded as `UUID{ low: 0x7243cc580bc17af4, high: 0x79d4c3b2020e67a5 }` message UUID { required uint64 low = 1; required uint64 high = 2;