From 693d81d5f9d35586ca463d1f32592717e0f0aaab Mon Sep 17 00:00:00 2001 From: Jonathan Gangi Date: Thu, 19 Sep 2024 18:34:25 -0300 Subject: [PATCH] Add models for StArMap APIv2 Query This commit introduces the following new models for the StArMap APIv2: - `QueryResponseContainer`: holds the whole Query APIv2 response within its property `responses` which is a list of `QueryResponseEntity`. - `QueryResponseEntity`: Represents a single mapping for a given workflow, name and cloud. It can be converted to the `QueryResponse` from APIv1 model which works in an analogue way (without support for cloud name as a property). - `MappingResponseObject`: Represent a single mapping entity which is part of `QueryResponseEntity`. It holds a list of `Destination` object, similar to what `QueryResponse` does on its `clouds` attribute. - `BillingCodeRule`: A dedicated model to represent a Billing Code Rule for the comnmunity workflow. Refers to SPSTRAT-382 --- starmap_client/models.py | 276 +++++++++++++++++- starmap_client/utils.py | 25 ++ .../{query => query_v1}/invalid_quer1.json | 0 .../{query => query_v1}/invalid_quer2.json | 0 .../data/{query => query_v1}/valid_quer1.json | 0 .../data/{query => query_v1}/valid_quer2.json | 0 .../data/{query => query_v1}/valid_quer3.json | 0 .../data/{query => query_v1}/valid_quer4.json | 0 .../data/{query => query_v1}/valid_quer5.json | 0 .../mapping_response_obj/invalid_mro1.json | 12 + .../mapping_response_obj/invalid_mro2.json | 4 + .../mapping_response_obj/invalid_mro3.json | 13 + .../mapping_response_obj/valid_mro1.json | 14 + .../mapping_response_obj/valid_mro1_meta.json | 3 + .../mapping_response_obj/valid_mro2.json | 16 + .../mapping_response_obj/valid_mro2_meta.json | 4 + .../mapping_response_obj/valid_mro3.json | 11 + .../mapping_response_obj/valid_mro3_meta.json | 3 + .../invalid_qrc1.json | 36 +++ .../query_response_container/valid_qrc1.json | 116 ++++++++ .../query_response_container/valid_qrc2.json | 78 +++++ .../query_response_container/valid_qrc3.json | 154 ++++++++++ .../query_response_entity/valid_qre1.json | 36 +++ .../valid_qre1_meta.json | 10 + .../query_response_entity/valid_qre2.json | 40 +++ .../valid_qre2_meta.json | 8 + .../query_response_entity/valid_qre3.json | 43 +++ .../valid_qre3_meta.json | 8 + .../query_response_entity/valid_qre4.json | 74 +++++ .../valid_qre4_meta.json | 10 + .../valid_qre_converted_old_api.json | 35 +++ ...valid_qre_converted_old_api_community.json | 166 +++++++++++ tests/test_client.py | 12 +- tests/test_models.py | 204 ++++++++++++- tests/test_utils.py | 30 ++ 35 files changed, 1423 insertions(+), 18 deletions(-) create mode 100644 starmap_client/utils.py rename tests/data/{query => query_v1}/invalid_quer1.json (100%) rename tests/data/{query => query_v1}/invalid_quer2.json (100%) rename tests/data/{query => query_v1}/valid_quer1.json (100%) rename tests/data/{query => query_v1}/valid_quer2.json (100%) rename tests/data/{query => query_v1}/valid_quer3.json (100%) rename tests/data/{query => query_v1}/valid_quer4.json (100%) rename tests/data/{query => query_v1}/valid_quer5.json (100%) create mode 100644 tests/data/query_v2/mapping_response_obj/invalid_mro1.json create mode 100644 tests/data/query_v2/mapping_response_obj/invalid_mro2.json create mode 100644 tests/data/query_v2/mapping_response_obj/invalid_mro3.json create mode 100644 tests/data/query_v2/mapping_response_obj/valid_mro1.json create mode 100644 tests/data/query_v2/mapping_response_obj/valid_mro1_meta.json create mode 100644 tests/data/query_v2/mapping_response_obj/valid_mro2.json create mode 100644 tests/data/query_v2/mapping_response_obj/valid_mro2_meta.json create mode 100644 tests/data/query_v2/mapping_response_obj/valid_mro3.json create mode 100644 tests/data/query_v2/mapping_response_obj/valid_mro3_meta.json create mode 100644 tests/data/query_v2/query_response_container/invalid_qrc1.json create mode 100644 tests/data/query_v2/query_response_container/valid_qrc1.json create mode 100644 tests/data/query_v2/query_response_container/valid_qrc2.json create mode 100644 tests/data/query_v2/query_response_container/valid_qrc3.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre1.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre1_meta.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre2.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre2_meta.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre3.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre3_meta.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre4.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre4_meta.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre_converted_old_api.json create mode 100644 tests/data/query_v2/query_response_entity/valid_qre_converted_old_api_community.json create mode 100644 tests/test_utils.py diff --git a/starmap_client/models.py b/starmap_client/models.py index c251786..3f69353 100644 --- a/starmap_client/models.py +++ b/starmap_client/models.py @@ -1,26 +1,35 @@ # SPDX-License-Identifier: GPL-3.0-or-later import sys +from copy import deepcopy from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Type if sys.version_info >= (3, 8): from typing import TypedDict # pragma: no cover else: from typing_extensions import TypedDict # pragma: no cover -from attrs import Attribute, Factory, field, frozen +from attrs import Attribute, Factory, asdict, evolve, field, frozen from attrs.validators import deep_iterable, deep_mapping, instance_of, min_len, optional +from starmap_client.utils import assert_is_dict, dict_union + __all__ = [ + 'BillingCodeRule', 'Destination', 'Mapping', 'Policy', 'QueryResponse', + 'QueryResponseEntity', + 'QueryResponseContainer', 'PaginatedRawData', 'PaginationMetadata', ] +# ============================================ Common ============================================== + + class PaginationMetadata(TypedDict): """Datastructure of the metadata about the paginated query.""" @@ -222,6 +231,9 @@ class Policy(StarmapBaseData): """The policy workflow name.""" +# ============================================ APIv1 =============================================== + + @frozen class QueryResponse(StarmapJSONDecodeMixin): """Represent a query response from StArMap.""" @@ -262,3 +274,263 @@ def _preprocess_json(cls, json: Any) -> Dict[str, Any]: mappings[c] = dst json["clouds"] = mappings return json + + +# ============================================ APIv2 =============================================== + + +class BillingImageType(str, Enum): + """Define the image type for :class:`~BillingCodeRule` for APIv2.""" + + access = "access" + hourly = "hourly" + marketplace = "marketplace" + + +@frozen +class BillingCodeRule(StarmapJSONDecodeMixin): + """Define a single Billing Code Configuration rule for APIv2.""" + + codes: List[str] = field( + validator=deep_iterable( + member_validator=instance_of(str), iterable_validator=instance_of(list) + ) + ) + """The billing codes to insert when this rule is matched.""" + + image_name: str = field(validator=instance_of(str)) + """The image name to match the rule.""" + + image_types: List[BillingImageType] = field( + converter=lambda x: [BillingImageType[d] for d in x], + validator=deep_iterable( + member_validator=instance_of(BillingImageType), iterable_validator=instance_of(list) + ), + ) + """Image types list. Supported values are ``access`` and ``hourly``.""" + + name: Optional[str] + """The billing code rule name.""" + + +@frozen +class MappingResponseObject(StarmapJSONDecodeMixin, MetaMixin): + """Represent a single mapping response from :class:`~QueryResponseObject` for APIv2.""" + + destinations: List[Destination] = field( + validator=deep_iterable( + iterable_validator=instance_of(list), member_validator=instance_of(Destination) + ) + ) + """List of destinations for the mapping response object.""" + + provider: Optional[str] = field(validator=optional(instance_of(str))) + """The provider name for the community workflow.""" + + @staticmethod + def _unify_meta_with_destinations(json: Dict[str, Any]) -> None: + """Merge the ``meta`` data from mappings into the destinations.""" + destinations = json.get("destinations", []) + if not isinstance(destinations, list): + raise ValueError(f"Expected destinations to be a list, got \"{type(destinations)}\"") + meta = json.get("meta", {}) + for d in destinations: + d["meta"] = dict_union(meta, d.get("meta", {})) + + @classmethod + def _preprocess_json(cls, json: Any) -> Dict[str, Any]: + """ + Properly adjust the Destinations list for building this object. + + Params: + json (dict): A JSON containing a StArMap Query response. + Returns: + dict: The modified JSON. + """ + cls._unify_meta_with_destinations(json) + provider = json.get("provider", None) + destinations = json.get("destinations", []) + # _unify_meta_lower_entity(json, "destinations") + converted_destinations = [] + for d in destinations: + d["provider"] = provider + converted_destinations.append(Destination.from_json(d)) + json["destinations"] = converted_destinations + return json + + +@frozen +class QueryResponseEntity(StarmapJSONDecodeMixin, MetaMixin): + """Represent a single query response entity from StArMap APIv2.""" + + name: str = field(validator=instance_of(str)) + """The :class:`~Policy` name.""" + + billing_code_config: Optional[Dict[str, BillingCodeRule]] = field( + validator=optional( + deep_mapping( + key_validator=instance_of(str), + value_validator=instance_of(BillingCodeRule), + mapping_validator=instance_of(dict), + ) + ) + ) + """The Billing Code Configuration for the community workflow.""" + + cloud: str = field(validator=instance_of(str)) + """The cloud name where the destinations are meant to.""" + + workflow: Workflow = field(converter=lambda x: Workflow(x)) + """The :class:`~Policy` workflow.""" + + mappings: Dict[str, MappingResponseObject] = field( + validator=deep_mapping( + key_validator=instance_of(str), + value_validator=instance_of(MappingResponseObject), + mapping_validator=instance_of(dict), + ), + ) + """Dictionary with the cloud account names and MappingResponseObjects.""" + + @property + def account_names(self) -> List[str]: + """Return the list of cloud account names declared on ``mappings``.""" + return list(self.mappings.keys()) + + @property + def all_mappings(self) -> List[MappingResponseObject]: + """Return all ``MappingResponseObject`` stored in ``mappings``.""" + return list(self.mappings.values()) + + def get_mapping_for_account(self, account: str) -> MappingResponseObject: + """Return a single ``MappingResponseObject`` for a given account name. + + Args: + account (str): + The account name to retrieve the ``MappingResponseObject`` + Returns: + MappingResponseObject: The required mapping when found + Raises: KeyError when not found + """ + obj = self.mappings.get(account, None) + if not obj: + raise KeyError(f"No mappings found for account name {account}") + return obj + + def to_classic_query_response(self) -> QueryResponse: + """Return the representation of this object as a :class:`~QueryResponse` from APIv1.""" + + def add_bc_to_dst_meta(clouds: Dict[str, List[Destination]]): + bc_data = {k: asdict(v) for k, v in self.billing_code_config.items()} + for dst_list in clouds.values(): + for d in dst_list: + meta = d.meta or {} + meta["billing-code-config"] = bc_data + d = evolve(d, meta=meta) + + clouds: Dict[str, List[Destination]] = {} + for k, v in self.mappings.items(): + clouds[k] = [deepcopy(d) for d in v.destinations] + + if self.billing_code_config: + add_bc_to_dst_meta(clouds) + + return QueryResponse(name=self.name, workflow=self.workflow, clouds=clouds) + + @staticmethod + def _unify_meta_with_mappings(json: Dict[str, Any]) -> None: + """Merge the ``meta`` data from package into the mappings.""" + mappings = json.get("mappings", {}) + meta = json.get("meta", {}) + for k, v in mappings.items(): + mappings[k]["meta"] = dict_union(meta, v.get("meta", {})) + + @classmethod + def _preprocess_json(cls, json: Dict[str, Any]) -> Dict[str, Any]: + """ + Properly adjust the MappingResponseObject list and BillingCodeConfig dict for building this object. + + Params: + json (dict): A JSON containing a StArMap Query response. + Returns: + dict: The modified JSON. + """ # noqa: D202 E501 + + def parse_entity_build_obj( + entity_name: str, converter_type: Type[StarmapJSONDecodeMixin] + ) -> None: + entity = json.pop(entity_name, {}) + for k in entity.keys(): + assert_is_dict(entity[k]) + obj = converter_type.from_json(entity[k]) + entity[k] = obj + json[entity_name] = entity + + bcc = json.pop("billing-code-config", {}) + json["billing_code_config"] = bcc + cls._unify_meta_with_mappings(json) + parse_entity_build_obj("mappings", MappingResponseObject) + parse_entity_build_obj("billing_code_config", BillingCodeRule) + return json + + +@frozen +class QueryResponseContainer: + """Represent a full query response from APIv2.""" + + responses: List[QueryResponseEntity] = field( + validator=deep_iterable( + member_validator=instance_of(QueryResponseEntity), iterable_validator=instance_of(list) + ) + ) + """List with all responses from a Query V2 mapping.""" + + @classmethod + def from_json(cls, json: Any): + """ + Convert the APIv2 response JSON into this object. + + Args: + json (list) + A JSON containing a StArMap APIv2 response. + Returns: + The converted object from JSON. + """ + if not isinstance(json, list): + raise ValueError(f"Expected root to be a list, got \"{type(json)}\".") + + responses = [QueryResponseEntity.from_json(qre) for qre in json] + return cls(responses) + + def filter_by_workflow(self, workflow: Workflow) -> List[QueryResponseEntity]: + """Return a sublist of the responses with only the selected workflow. + + Args: + workflow (Workflow): + The workflow to filter the list of responses + Returns: + list: The sublist with only the selected workflows + """ + return [x for x in self.responses if x.workflow == workflow] + + def filter_by_cloud(self, cloud: str) -> List[QueryResponseEntity]: + """Return a sublist of the responses with only the selected cloud name. + + Args: + cloud (str): + The cloud name to filter the list of responses + Returns: + list: The sublist with only the selected cloud name. + """ + return [x for x in self.responses if x.cloud == cloud] + + def filter_by(self, **kwargs): + """Return a sublist of the responses with the selected filters.""" + filters = { + "cloud": self.filter_by_cloud, + "workflow": self.filter_by_workflow, + } + res = [] + for k, v in kwargs.items(): + res.extend(filters[k](v)) + return res diff --git a/starmap_client/utils.py b/starmap_client/utils.py new file mode 100644 index 0000000..4092ad9 --- /dev/null +++ b/starmap_client/utils.py @@ -0,0 +1,25 @@ +from typing import Any, Dict + + +def assert_is_dict(data: Any) -> None: + """Ensure the incoming data is a dictionary, raises ``ValueError`` if not.""" + if not isinstance(data, dict): + raise ValueError(f"Expected dictionary, got {type(data)}: {data}") + + +def dict_union(a: Dict[str, Any], b: Dict[str, Any]) -> Dict[str, Any]: + """Return a new dictionary with the combination of A and B. + + Args: + a (dict): + The left dictionary to be combined + b (dict): + The right dictionary to be combined. It will override the same keys from A. + Returns: + dict: A new dictionary with combination of A and B. + """ + c = {} + for x in [a, b]: + assert_is_dict(x) + c.update(x) + return c diff --git a/tests/data/query/invalid_quer1.json b/tests/data/query_v1/invalid_quer1.json similarity index 100% rename from tests/data/query/invalid_quer1.json rename to tests/data/query_v1/invalid_quer1.json diff --git a/tests/data/query/invalid_quer2.json b/tests/data/query_v1/invalid_quer2.json similarity index 100% rename from tests/data/query/invalid_quer2.json rename to tests/data/query_v1/invalid_quer2.json diff --git a/tests/data/query/valid_quer1.json b/tests/data/query_v1/valid_quer1.json similarity index 100% rename from tests/data/query/valid_quer1.json rename to tests/data/query_v1/valid_quer1.json diff --git a/tests/data/query/valid_quer2.json b/tests/data/query_v1/valid_quer2.json similarity index 100% rename from tests/data/query/valid_quer2.json rename to tests/data/query_v1/valid_quer2.json diff --git a/tests/data/query/valid_quer3.json b/tests/data/query_v1/valid_quer3.json similarity index 100% rename from tests/data/query/valid_quer3.json rename to tests/data/query_v1/valid_quer3.json diff --git a/tests/data/query/valid_quer4.json b/tests/data/query_v1/valid_quer4.json similarity index 100% rename from tests/data/query/valid_quer4.json rename to tests/data/query_v1/valid_quer4.json diff --git a/tests/data/query/valid_quer5.json b/tests/data/query_v1/valid_quer5.json similarity index 100% rename from tests/data/query/valid_quer5.json rename to tests/data/query_v1/valid_quer5.json diff --git a/tests/data/query_v2/mapping_response_obj/invalid_mro1.json b/tests/data/query_v2/mapping_response_obj/invalid_mro1.json new file mode 100644 index 0000000..fa464e0 --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/invalid_mro1.json @@ -0,0 +1,12 @@ +{ + "destinations": { + "architecture": "x86_64", + "destination": "fake_destination", + "overwrite": false, + "restrict_version": true, + "meta": { + "description": "Description on destination level." + } + }, + "error": "Expected destinations to be a list, got \"\"" +} \ No newline at end of file diff --git a/tests/data/query_v2/mapping_response_obj/invalid_mro2.json b/tests/data/query_v2/mapping_response_obj/invalid_mro2.json new file mode 100644 index 0000000..0dde02d --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/invalid_mro2.json @@ -0,0 +1,4 @@ +{ + "destinations": true, + "error": "Expected destinations to be a list, got \"\"" +} \ No newline at end of file diff --git a/tests/data/query_v2/mapping_response_obj/invalid_mro3.json b/tests/data/query_v2/mapping_response_obj/invalid_mro3.json new file mode 100644 index 0000000..a8ca886 --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/invalid_mro3.json @@ -0,0 +1,13 @@ +{ + "destinations": [ + { + "architecture": "x86_64", + "destination": "fake_destination", + "overwrite": false, + "restrict_version": true + } + ], + "meta": [], + "provider": "AWS", + "error": "Expected dictionary, got : \\[\\]" +} diff --git a/tests/data/query_v2/mapping_response_obj/valid_mro1.json b/tests/data/query_v2/mapping_response_obj/valid_mro1.json new file mode 100644 index 0000000..7652c21 --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/valid_mro1.json @@ -0,0 +1,14 @@ +{ + "destinations": [ + { + "architecture": "x86_64", + "destination": "fake_destination", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "Description on map level." + }, + "provider": "AWS" +} diff --git a/tests/data/query_v2/mapping_response_obj/valid_mro1_meta.json b/tests/data/query_v2/mapping_response_obj/valid_mro1_meta.json new file mode 100644 index 0000000..f384c34 --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/valid_mro1_meta.json @@ -0,0 +1,3 @@ +{ + "description": "Description on map level." +} diff --git a/tests/data/query_v2/mapping_response_obj/valid_mro2.json b/tests/data/query_v2/mapping_response_obj/valid_mro2.json new file mode 100644 index 0000000..5889b6d --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/valid_mro2.json @@ -0,0 +1,16 @@ +{ + "destinations": [ + { + "architecture": "x86_64", + "destination": "fake_destination", + "overwrite": false, + "restrict_version": true, + "meta": {"description": "Description on destination level."} + } + ], + "meta": { + "description": "Description on map level.", + "foo": "bar" + }, + "provider": "AZURE" +} diff --git a/tests/data/query_v2/mapping_response_obj/valid_mro2_meta.json b/tests/data/query_v2/mapping_response_obj/valid_mro2_meta.json new file mode 100644 index 0000000..80082fe --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/valid_mro2_meta.json @@ -0,0 +1,4 @@ +{ + "description": "Description on destination level.", + "foo": "bar" +} diff --git a/tests/data/query_v2/mapping_response_obj/valid_mro3.json b/tests/data/query_v2/mapping_response_obj/valid_mro3.json new file mode 100644 index 0000000..07495cc --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/valid_mro3.json @@ -0,0 +1,11 @@ +{ + "destinations": [ + { + "architecture": "x86_64", + "destination": "fake_destination", + "overwrite": false, + "restrict_version": true, + "meta": {"description": "Description on destination level."} + } + ] +} diff --git a/tests/data/query_v2/mapping_response_obj/valid_mro3_meta.json b/tests/data/query_v2/mapping_response_obj/valid_mro3_meta.json new file mode 100644 index 0000000..4816b7a --- /dev/null +++ b/tests/data/query_v2/mapping_response_obj/valid_mro3_meta.json @@ -0,0 +1,3 @@ +{ + "description": "Description on destination level." +} diff --git a/tests/data/query_v2/query_response_container/invalid_qrc1.json b/tests/data/query_v2/query_response_container/invalid_qrc1.json new file mode 100644 index 0000000..92ac197 --- /dev/null +++ b/tests/data/query_v2/query_response_container/invalid_qrc1.json @@ -0,0 +1,36 @@ +{ + "cloud": "test", + "mappings": { + "test-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination1", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "EMEA description" + }, + "provider": null + }, + "test-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination2", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "NA description" + }, + "provider": null + } + }, + "meta": {"foo": "bar"}, + "name": "product-test", + "workflow": "stratosphere" +} diff --git a/tests/data/query_v2/query_response_container/valid_qrc1.json b/tests/data/query_v2/query_response_container/valid_qrc1.json new file mode 100644 index 0000000..f47be56 --- /dev/null +++ b/tests/data/query_v2/query_response_container/valid_qrc1.json @@ -0,0 +1,116 @@ +[ + { + "cloud": "test", + "mappings": { + "test-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination1", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "EMEA description" + }, + "provider": null + }, + "test-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination2", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "NA description" + }, + "provider": null + } + }, + "meta": { + "foo": "bar" + }, + "name": "product-test", + "workflow": "stratosphere" + }, + { + "cloud": "test", + "billing-code-config": { + "test-access": { + "codes": [ + "code-000" + ], + "image_name": "test", + "image_types": [ + "access" + ], + "name": "Access2" + }, + "test-marketplace": { + "codes": [], + "image_name": "test", + "image_types": [ + "marketplace" + ], + "name": "Marketplace" + }, + "test-hourly": { + "codes": [ + "code-0002" + ], + "image_name": "rhel", + "image_types": [ + "hourly" + ], + "name": "Hourly2" + } + }, + "mappings": { + "test-storage": { + "destinations": [ + { + "destination": "test-destination/foo/bar", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "second-test-destination/foo/bar", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + }, + "another-storage": { + "destinations": [ + { + "destination": "aaaaaaaaaaaaaaa", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "bbbbbbbbbbbbb", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + } + }, + "workflow": "community", + "meta": { + "lorem": "ipsum" + }, + "name": "sample-product" + } +] diff --git a/tests/data/query_v2/query_response_container/valid_qrc2.json b/tests/data/query_v2/query_response_container/valid_qrc2.json new file mode 100644 index 0000000..c1396fe --- /dev/null +++ b/tests/data/query_v2/query_response_container/valid_qrc2.json @@ -0,0 +1,78 @@ +[ + { + "cloud": "test", + "mappings": { + "test-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination1", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "EMEA description" + }, + "provider": null + }, + "test-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination2", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "NA description" + }, + "provider": null + } + }, + "meta": { + "foo": "bar" + }, + "name": "product-test", + "workflow": "stratosphere" + }, + { + "cloud": "anothertest", + "mappings": { + "anothertest-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination3", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "EMEA description" + }, + "provider": null + }, + "anothertest-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination4", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "NA description" + }, + "provider": null + } + }, + "meta": { + "foo": "bar" + }, + "name": "product-test", + "workflow": "stratosphere" + } +] diff --git a/tests/data/query_v2/query_response_container/valid_qrc3.json b/tests/data/query_v2/query_response_container/valid_qrc3.json new file mode 100644 index 0000000..891cdb4 --- /dev/null +++ b/tests/data/query_v2/query_response_container/valid_qrc3.json @@ -0,0 +1,154 @@ +[ + { + "cloud": "test", + "mappings": { + "test-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination1", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "EMEA description" + }, + "provider": null + }, + "test-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination2", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "NA description" + }, + "provider": null + } + }, + "meta": { + "foo": "bar" + }, + "name": "product-test", + "workflow": "stratosphere" + }, + { + "cloud": "test", + "billing-code-config": { + "test-access": { + "codes": [ + "code-000" + ], + "image_name": "test", + "image_types": [ + "access" + ], + "name": "Access2" + }, + "test-marketplace": { + "codes": [], + "image_name": "test", + "image_types": [ + "marketplace" + ], + "name": "Marketplace" + }, + "test-hourly": { + "codes": [ + "code-0002" + ], + "image_name": "rhel", + "image_types": [ + "hourly" + ], + "name": "Hourly2" + } + }, + "mappings": { + "test-storage": { + "destinations": [ + { + "destination": "test-destination/foo/bar", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "second-test-destination/foo/bar", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + }, + "another-storage": { + "destinations": [ + { + "destination": "aaaaaaaaaaaaaaa", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "bbbbbbbbbbbbb", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + } + }, + "workflow": "community", + "meta": { + "lorem": "ipsum" + }, + "name": "sample-product" + }, + { + "cloud": "anothertest", + "mappings": { + "anothertest-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination3", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "EMEA description" + }, + "provider": null + }, + "anothertest-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination4", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "NA description" + }, + "provider": null + } + }, + "meta": { + "foo": "bar" + }, + "name": "product-test", + "workflow": "stratosphere" + } +] diff --git a/tests/data/query_v2/query_response_entity/valid_qre1.json b/tests/data/query_v2/query_response_entity/valid_qre1.json new file mode 100644 index 0000000..92ac197 --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre1.json @@ -0,0 +1,36 @@ +{ + "cloud": "test", + "mappings": { + "test-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination1", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "EMEA description" + }, + "provider": null + }, + "test-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "destination2", + "overwrite": false, + "restrict_version": true + } + ], + "meta": { + "description": "NA description" + }, + "provider": null + } + }, + "meta": {"foo": "bar"}, + "name": "product-test", + "workflow": "stratosphere" +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre1_meta.json b/tests/data/query_v2/query_response_entity/valid_qre1_meta.json new file mode 100644 index 0000000..588a8eb --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre1_meta.json @@ -0,0 +1,10 @@ +{ + "test-emea": { + "description": "EMEA description", + "foo": "bar" + }, + "test-na": { + "description": "NA description", + "foo": "bar" + } +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre2.json b/tests/data/query_v2/query_response_entity/valid_qre2.json new file mode 100644 index 0000000..ce65f24 --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre2.json @@ -0,0 +1,40 @@ +{ + "cloud": "test", + "mappings": { + "test-marketplace": { + "destinations": [ + { + "destination": "test-destination/foo/bar", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "second-test-destination/foo/bar", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null + }, + "another-marketplace": { + "destinations": [ + { + "destination": "aaaaaaaaaaaaaaa", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "bbbbbbbbbbbbb", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null + } + }, + "workflow": "stratosphere", + "name": "sample-product", + "meta": { + "foo": "bar" + } +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre2_meta.json b/tests/data/query_v2/query_response_entity/valid_qre2_meta.json new file mode 100644 index 0000000..c994957 --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre2_meta.json @@ -0,0 +1,8 @@ +{ + "test-marketplace": { + "foo": "bar" + }, + "another-marketplace": { + "foo": "bar" + } +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre3.json b/tests/data/query_v2/query_response_entity/valid_qre3.json new file mode 100644 index 0000000..a5b63f7 --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre3.json @@ -0,0 +1,43 @@ +{ + "cloud": "test", + "mappings": { + "test-marketplace": { + "destinations": [ + { + "destination": "test-destination/foo/bar", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "second-test-destination/foo/bar", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + }, + "another-marketplace": { + "destinations": [ + { + "destination": "aaaaaaaaaaaaaaa", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "bbbbbbbbbbbbb", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + } + }, + "workflow": "stratosphere", + "name": "sample-product" +} \ No newline at end of file diff --git a/tests/data/query_v2/query_response_entity/valid_qre3_meta.json b/tests/data/query_v2/query_response_entity/valid_qre3_meta.json new file mode 100644 index 0000000..c994957 --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre3_meta.json @@ -0,0 +1,8 @@ +{ + "test-marketplace": { + "foo": "bar" + }, + "another-marketplace": { + "foo": "bar" + } +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre4.json b/tests/data/query_v2/query_response_entity/valid_qre4.json new file mode 100644 index 0000000..106ee0b --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre4.json @@ -0,0 +1,74 @@ +{ + "cloud": "test", + "billing-code-config": { + "test-access": { + "codes": [ + "code-000" + ], + "image_name": "test", + "image_types": [ + "access" + ], + "name": "Access2" + }, + "test-marketplace": { + "codes": [], + "image_name": "test", + "image_types": [ + "marketplace" + ], + "name": "Marketplace" + }, + "test-hourly": { + "codes": [ + "code-0002" + ], + "image_name": "rhel", + "image_types": [ + "hourly" + ], + "name": "Hourly2" + } + }, + "mappings": { + "test-storage": { + "destinations": [ + { + "destination": "test-destination/foo/bar", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "second-test-destination/foo/bar", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + }, + "another-storage": { + "destinations": [ + { + "destination": "aaaaaaaaaaaaaaa", + "overwrite": false, + "restrict_version": false + }, + { + "destination": "bbbbbbbbbbbbb", + "overwrite": true, + "restrict_version": false + } + ], + "provider": null, + "meta": { + "foo": "bar" + } + } + }, + "workflow": "community", + "meta": {"lorem": "ipsum"}, + "name": "sample-product" +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre4_meta.json b/tests/data/query_v2/query_response_entity/valid_qre4_meta.json new file mode 100644 index 0000000..06f27fe --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre4_meta.json @@ -0,0 +1,10 @@ +{ + "test-storage": { + "foo": "bar", + "lorem": "ipsum" + }, + "another-storage": { + "foo": "bar", + "lorem": "ipsum" + } +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre_converted_old_api.json b/tests/data/query_v2/query_response_entity/valid_qre_converted_old_api.json new file mode 100644 index 0000000..794db70 --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre_converted_old_api.json @@ -0,0 +1,35 @@ +{ + "mappings": + { + "test-marketplace": [ + { + "destination": "test-destination/foo/bar", + "overwrite": false, + "restrict_version": false, + "meta": {"foo": "bar"} + }, + { + "destination": "second-test-destination/foo/bar", + "overwrite": true, + "restrict_version": false, + "meta": {"foo": "bar"} + } + ], + "another-marketplace": [ + { + "destination": "aaaaaaaaaaaaaaa", + "overwrite": false, + "restrict_version": false, + "meta": {"foo": "bar"} + }, + { + "destination": "bbbbbbbbbbbbb", + "overwrite": true, + "restrict_version": false, + "meta": {"foo": "bar"} + } + ] + }, + "workflow": "stratosphere", + "name": "sample-product" +} diff --git a/tests/data/query_v2/query_response_entity/valid_qre_converted_old_api_community.json b/tests/data/query_v2/query_response_entity/valid_qre_converted_old_api_community.json new file mode 100644 index 0000000..15a315d --- /dev/null +++ b/tests/data/query_v2/query_response_entity/valid_qre_converted_old_api_community.json @@ -0,0 +1,166 @@ +{ + "mappings": { + "test-storage": [ + { + "destination": "test-destination/foo/bar", + "overwrite": false, + "restrict_version": false, + "meta": { + "billing-code-config": { + "test-access": { + "codes": [ + "code-000" + ], + "image_name": "test", + "image_types": [ + "access" + ], + "name": "Access2" + }, + "test-marketplace": { + "codes": [], + "image_name": "test", + "image_types": [ + "marketplace" + ], + "name": "Marketplace" + }, + "test-hourly": { + "codes": [ + "code-0002" + ], + "image_name": "rhel", + "image_types": [ + "hourly" + ], + "name": "Hourly2" + } + }, + "foo": "bar", + "lorem": "ipsum" + } + }, + { + "destination": "second-test-destination/foo/bar", + "overwrite": true, + "restrict_version": false, + "meta": { + "billing-code-config": { + "test-access": { + "codes": [ + "code-000" + ], + "image_name": "test", + "image_types": [ + "access" + ], + "name": "Access2" + }, + "test-marketplace": { + "codes": [], + "image_name": "test", + "image_types": [ + "marketplace" + ], + "name": "Marketplace" + }, + "test-hourly": { + "codes": [ + "code-0002" + ], + "image_name": "rhel", + "image_types": [ + "hourly" + ], + "name": "Hourly2" + } + }, + "foo": "bar", + "lorem": "ipsum" + } + } + ], + "another-storage": [ + { + "destination": "aaaaaaaaaaaaaaa", + "overwrite": false, + "restrict_version": false, + "meta": { + "billing-code-config": { + "test-access": { + "codes": [ + "code-000" + ], + "image_name": "test", + "image_types": [ + "access" + ], + "name": "Access2" + }, + "test-marketplace": { + "codes": [], + "image_name": "test", + "image_types": [ + "marketplace" + ], + "name": "Marketplace" + }, + "test-hourly": { + "codes": [ + "code-0002" + ], + "image_name": "rhel", + "image_types": [ + "hourly" + ], + "name": "Hourly2" + } + }, + "foo": "bar", + "lorem": "ipsum" + } + }, + { + "destination": "bbbbbbbbbbbbb", + "overwrite": true, + "restrict_version": false, + "meta": { + "billing-code-config": { + "test-access": { + "codes": [ + "code-000" + ], + "image_name": "test", + "image_types": [ + "access" + ], + "name": "Access2" + }, + "test-marketplace": { + "codes": [], + "image_name": "test", + "image_types": [ + "marketplace" + ], + "name": "Marketplace" + }, + "test-hourly": { + "codes": [ + "code-0002" + ], + "image_name": "rhel", + "image_types": [ + "hourly" + ], + "name": "Hourly2" + } + }, + "foo": "bar", + "lorem": "ipsum" + } + } + ] + }, + "workflow": "community", + "name": "sample-product" +} \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index e6d918a..79aff5e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -46,7 +46,7 @@ def tearDown(self): mock.patch.stopall() def test_query_image_success(self): - fpath = "tests/data/query/valid_quer1.json" + fpath = "tests/data/query_v1/valid_quer1.json" self.mock_resp_success.json.return_value = load_json(fpath) self.mock_session.get.return_value = self.mock_resp_success @@ -59,7 +59,7 @@ def test_query_image_success(self): self.assertEqual(res, QueryResponse.from_json(load_json(fpath))) def test_in_memory_query_image(self): - fpath = "tests/data/query/valid_quer1.json" + fpath = "tests/data/query_v1/valid_quer1.json" data = [QueryResponse.from_json(load_json(fpath))] provider = InMemoryMapProvider(data) @@ -84,7 +84,7 @@ def test_query_image_not_found(self): self.assertIsNone(res) def test_query_image_by_name(self): - fpath = "tests/data/query/valid_quer1.json" + fpath = "tests/data/query_v1/valid_quer1.json" self.mock_resp_success.json.return_value = load_json(fpath) self.mock_session.get.return_value = self.mock_resp_success @@ -97,7 +97,7 @@ def test_query_image_by_name(self): self.assertEqual(res, QueryResponse.from_json(load_json(fpath))) def test_in_memory_query_image_by_name(self): - fpath = "tests/data/query/valid_quer1.json" + fpath = "tests/data/query_v1/valid_quer1.json" data = [QueryResponse.from_json(load_json(fpath))] provider = InMemoryMapProvider(data) @@ -108,7 +108,7 @@ def test_in_memory_query_image_by_name(self): self.assertEqual(res, data[0]) def test_query_image_by_name_version(self): - fpath = "tests/data/query/valid_quer1.json" + fpath = "tests/data/query_v1/valid_quer1.json" self.mock_resp_success.json.return_value = load_json(fpath) self.mock_session.get.return_value = self.mock_resp_success @@ -337,7 +337,7 @@ def test_client_requires_url_or_session(self): def test_offline_client(): """Ensure the cient can be used offline with a local provider.""" - fpath = "tests/data/query/valid_quer1.json" + fpath = "tests/data/query_v1/valid_quer1.json" qr = QueryResponse.from_json(load_json(fpath)) # The provider will have the QueryResponse from fpath diff --git a/tests/test_models.py b/tests/test_models.py index e214481..4ddacab 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,10 +1,21 @@ import json +from copy import deepcopy from typing import Any import pytest +from attrs import asdict from attrs.exceptions import FrozenInstanceError -from starmap_client.models import Destination, Mapping, Policy, QueryResponse +from starmap_client.models import ( + Destination, + Mapping, + MappingResponseObject, + Policy, + QueryResponse, + QueryResponseContainer, + QueryResponseEntity, + Workflow, +) def load_json(json_file: str) -> Any: @@ -181,15 +192,15 @@ def test_frozen_policy(self): p.name = "test" -class TestQueryResponse: +class TestV1QueryResponse: @pytest.mark.parametrize( "json_file", [ - "tests/data/query/valid_quer1.json", - "tests/data/query/valid_quer2.json", - "tests/data/query/valid_quer3.json", - "tests/data/query/valid_quer4.json", - "tests/data/query/valid_quer5.json", + "tests/data/query_v1/valid_quer1.json", + "tests/data/query_v1/valid_quer2.json", + "tests/data/query_v1/valid_quer3.json", + "tests/data/query_v1/valid_quer4.json", + "tests/data/query_v1/valid_quer5.json", ], ) def test_valid_query_resp_json(self, json_file: str) -> None: @@ -212,8 +223,8 @@ def test_valid_query_resp_json(self, json_file: str) -> None: @pytest.mark.parametrize( "json_file", [ - "tests/data/query/invalid_quer1.json", - "tests/data/query/invalid_quer2.json", + "tests/data/query_v1/invalid_quer1.json", + "tests/data/query_v1/invalid_quer2.json", ], ) def test_invalid_clouds(self, json_file) -> None: @@ -224,9 +235,182 @@ def test_invalid_clouds(self, json_file) -> None: QueryResponse.from_json(data) def test_frozen_query(self): - data = load_json("tests/data/query/valid_quer1.json") + data = load_json("tests/data/query_v1/valid_quer1.json") q = QueryResponse.from_json(data) with pytest.raises(FrozenInstanceError): q.name = "test" + + +class TestV2MappingResponseObject: + @pytest.mark.parametrize( + "json_file,meta,provider", + [ + ( + "tests/data/query_v2/mapping_response_obj/valid_mro1.json", + "tests/data/query_v2/mapping_response_obj/valid_mro1_meta.json", + "AWS", + ), + ( + "tests/data/query_v2/mapping_response_obj/valid_mro2.json", + "tests/data/query_v2/mapping_response_obj/valid_mro2_meta.json", + "AZURE", + ), + ( + "tests/data/query_v2/mapping_response_obj/valid_mro3.json", + "tests/data/query_v2/mapping_response_obj/valid_mro3_meta.json", + None, + ), + ], + ) + def test_valid_mapping_response_obj(self, json_file, meta, provider) -> None: + data = load_json(json_file) + expected_meta = load_json(meta) + + m = MappingResponseObject.from_json(data) + assert m.provider == provider + for d in m.destinations: + assert d.meta == expected_meta + assert d.provider == provider + + @pytest.mark.parametrize( + "json_file", + [ + "tests/data/query_v2/mapping_response_obj/invalid_mro1.json", + "tests/data/query_v2/mapping_response_obj/invalid_mro2.json", + "tests/data/query_v2/mapping_response_obj/invalid_mro3.json", + ], + ) + def test_invalid_clouds(self, json_file) -> None: + data = load_json(json_file) + err = data.pop("error") + + with pytest.raises((TypeError, ValueError), match=err): + MappingResponseObject.from_json(data) + + +class TestV2QueryResponseEntity: + @pytest.mark.parametrize( + "json_file,meta", + [ + ( + "tests/data/query_v2/query_response_entity/valid_qre1.json", + "tests/data/query_v2/query_response_entity/valid_qre1_meta.json", + ), + ( + "tests/data/query_v2/query_response_entity/valid_qre2.json", + "tests/data/query_v2/query_response_entity/valid_qre2_meta.json", + ), + ( + "tests/data/query_v2/query_response_entity/valid_qre3.json", + "tests/data/query_v2/query_response_entity/valid_qre3_meta.json", + ), + ( + "tests/data/query_v2/query_response_entity/valid_qre4.json", + "tests/data/query_v2/query_response_entity/valid_qre4_meta.json", + ), + ], + ) + def test_valid_query_response_entity(self, json_file, meta) -> None: + data = load_json(json_file) + d = deepcopy(data) + expected_meta_dict = load_json(meta) + + q = QueryResponseEntity.from_json(data) + for account_name in q.account_names: + assert q.mappings[account_name].meta == expected_meta_dict[account_name] + + if q.billing_code_config: + bc_asdict = {k: asdict(v) for k, v in q.billing_code_config.items()} + assert bc_asdict == d["billing-code-config"] + + def test_query_response_properties(self) -> None: + data = load_json("tests/data/query_v2/query_response_entity/valid_qre1.json") + q = QueryResponseEntity.from_json(data) + + assert q.account_names == ["test-emea", "test-na"] + assert q.all_mappings == list(q.mappings.values()) + + def test_get_mapping_for_account(self) -> None: + data = load_json("tests/data/query_v2/query_response_entity/valid_qre1.json") + q = QueryResponseEntity.from_json(data) + + assert q.get_mapping_for_account("test-na") == q.mappings["test-na"] + + with pytest.raises(KeyError): + q.get_mapping_for_account("foo-bar") + + @pytest.mark.parametrize( + "json_file,old_api_file", + [ + ( + "tests/data/query_v2/query_response_entity/valid_qre2.json", + "tests/data/query_v2/query_response_entity/valid_qre_converted_old_api.json", + ), + ( + "tests/data/query_v2/query_response_entity/valid_qre3.json", + "tests/data/query_v2/query_response_entity/valid_qre_converted_old_api.json", + ), + ( + "tests/data/query_v2/query_response_entity/valid_qre4.json", + "tests/data/query_v2/query_response_entity/valid_qre_converted_old_api_community.json", # noqa: E501 + ), + ], + ) + def test_to_classic_query_response(self, json_file, old_api_file) -> None: + old_api_data = load_json(old_api_file) + expected_qr = QueryResponse.from_json(old_api_data) + + data = load_json(json_file) + q = QueryResponseEntity.from_json(data) + + assert q.to_classic_query_response() == expected_qr + + +class TestV2QueryResponseContainer: + + @pytest.mark.parametrize( + "json_file", ["tests/data/query_v2/query_response_container/invalid_qrc1.json"] + ) + def test_invalid_query_response_container(self, json_file) -> None: + data = load_json(json_file) + err = f"Expected root to be a list, got \"{type(data)}\"" + with pytest.raises(ValueError, match=err): + QueryResponseContainer.from_json(data) + + def test_filter_by_workflow(self) -> None: + data = load_json("tests/data/query_v2/query_response_container/valid_qrc1.json") + qc = QueryResponseContainer.from_json(data) + + stt = QueryResponseEntity.from_json( + load_json("tests/data/query_v2/query_response_entity/valid_qre1.json") + ) + cmt = QueryResponseEntity.from_json( + load_json("tests/data/query_v2/query_response_entity/valid_qre4.json") + ) + + assert qc.filter_by_workflow(Workflow.stratosphere)[0] == stt + assert qc.filter_by_workflow(Workflow.community)[0] == cmt + assert qc.filter_by(workflow=Workflow.stratosphere)[0] == stt + assert qc.filter_by(workflow=Workflow.community)[0] == cmt + + def test_filter_by_cloud(self) -> None: + data = load_json("tests/data/query_v2/query_response_container/valid_qrc2.json") + qc = QueryResponseContainer.from_json(data) + + expected = QueryResponseEntity.from_json( + load_json("tests/data/query_v2/query_response_entity/valid_qre1.json") + ) + + assert qc.filter_by_cloud("test")[0] == expected + assert qc.filter_by(cloud="test")[0] == expected + + def test_multiple_filters(slef) -> None: + data = load_json("tests/data/query_v2/query_response_container/valid_qrc3.json") + qc = QueryResponseContainer.from_json(data) + expected = QueryResponseEntity.from_json( + load_json("tests/data/query_v2/query_response_entity/valid_qre1.json") + ) + + assert qc.filter_by(cloud="test", workflow=Workflow.stratosphere)[0] == expected diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..f6c0b7a --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,30 @@ +from typing import Any, Dict + +import pytest + +from starmap_client.utils import assert_is_dict, dict_union + + +def test_assert_is_dict() -> None: + assert_is_dict({}) + + for x in [[], (), "a", 1, 1.0, True]: + err = f"Expected dictionary, got {type(x)}: {x}" + err = err.replace("[", "\\[").replace("]", "\\]") # Avoid bad regex match + with pytest.raises(ValueError, match=err): + assert_is_dict(x) + + +@pytest.mark.parametrize( + "a,b,expected", + [ + ({}, {}, {}), + ({"foo": "bar"}, {}, {"foo": "bar"}), + ({}, {"foo": "bar"}, {"foo": "bar"}), + ({"1": 1, "3": 3}, {"2": 2}, {"1": 1, "2": 2, "3": 3}), + ({"1": 1, "3": 3}, {"2": 2, "3": 4}, {"1": 1, "2": 2, "3": 4}), + ({"A": True, "B": True, "C": True}, {"B": False}, {"A": True, "B": False, "C": True}), + ], +) +def test_dict_union(a: Dict[str, Any], b: Dict[str, Any], expected: Dict[str, Any]) -> None: + assert dict_union(a, b) == expected