From 2548c7e7d6aa306ca7453e1fc3ba735368b164ed Mon Sep 17 00:00:00 2001 From: Jonathan Rios Date: Thu, 13 Jul 2023 15:52:44 +0200 Subject: [PATCH] LITE-27893 Add validation step fro PPR upload --- Dockerfile | 2 +- connect_ext_ppr/constants.py | 362 +++++++++++++++++++++++++++ connect_ext_ppr/service.py | 20 ++ connect_ext_ppr/utils.py | 23 ++ poetry.lock | 203 ++++++++++++++- pyproject.toml | 2 + tests/conftest.py | 13 + tests/fixtures/ppr_valid_schema.json | 168 +++++++++++++ tests/fixtures/test_PPR_file.xlsx | Bin 0 -> 87657 bytes tests/test_service.py | 78 +++++- tests/test_utils.py | 65 +++++ 11 files changed, 933 insertions(+), 3 deletions(-) create mode 100644 connect_ext_ppr/constants.py create mode 100644 tests/fixtures/ppr_valid_schema.json create mode 100644 tests/fixtures/test_PPR_file.xlsx diff --git a/Dockerfile b/Dockerfile index a57c735..50ce613 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM cloudblueconnect/connect-extension-runner:28.4 +FROM cloudblueconnect/connect-extension-runner:28.5 COPY pyproject.toml /install_temp/. COPY poetry.* /install_temp/. diff --git a/connect_ext_ppr/constants.py b/connect_ext_ppr/constants.py new file mode 100644 index 0000000..0455a92 --- /dev/null +++ b/connect_ext_ppr/constants.py @@ -0,0 +1,362 @@ +# Common schemas +# Categories +CATEGORIES_SCHEMA = { + "type": "array", + "allOf": [ + {"contains": {"const": "Name_EN"}}, + {"contains": {"const": "Description_EN"}}, + ], + "items": { + "anyOf": [ + { + "pattern": "^(Description|Name)_[a-zA-Z]+$", + }, + { + "enum": [ + "Name_EN", + "Description_EN", + "ExpandCategory", + "DisplayInCCP", + "ParentCategory", + ], + }, + ], + }, + "uniqueItems": True, +} +# Policies +POLICIES_SCHEMA = { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ServicePlan", + "Period", + "PeriodType", + "Trial", + "Action", + "ActionPeriod", + "ActionPeriodTimezone", + "ApplicableTo", + ], + }, + "allOf": [ + {"contains": {"const": "ServicePlan"}}, + {"contains": {"const": "Period"}}, + {"contains": {"const": "PeriodType"}}, + {"contains": {"const": "Trial"}}, + {"contains": {"const": "Action"}}, + {"contains": {"const": "ActionPeriod"}}, + {"contains": {"const": "ActionPeriodTimezone"}}, + {"contains": {"const": "ApplicableTo"}}, + ], + "additionalItems": False, +} + +# Per Sheet Schema +RESOURCES_SCHEMA = { + "Resources": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "Name_EN", + "Description_EN", + "ResourceCategory", + "MPN", + "UOM", + "Measurable", + ], + }, + "allOf": [ + {"contains": {"const": "Name_EN"}}, + {"contains": {"const": "Description_EN"}}, + {"contains": {"const": "ResourceCategory"}}, + {"contains": {"const": "MPN"}}, + {"contains": {"const": "UOM"}}, + {"contains": {"const": "Measurable"}}, + ], + "additionalItems": False, + }, +} +SERVICE_PLANS_SCHEMA = { + "ServicePlans": { + "type": "array", + "items": { + "type": "string", + "anyOf": [ + { + "enum": [ + "OldName_1", + "Name_EN", + "Description_EN", + "PlanCategory", + "ServiceTerms", + "BillingPeriodDuration", + "BillingPeriodType", + "AlignBillingOrderWithStatementDay", + "NewDayOfStatement", + "AlignSalesOrderWithStatementDay", + "AllowScheduledChanges", + "BillingPeriodAlignment", + "CotermingPossibilities", + "ExpirationDateAlignedWithEndOfMonth", + "ExpirationDateAlignedWithSubscription", + "FirstBillingPeriodForFree", + "PricePeriod", + "RecurringType", + "AutoRenew", + "RenewOrderInterval", + "AutoRenewPlan", + "AutoRenewPeriod", + "AutoRenewPeriodType", + "BillingAlignmentResellerRedefineAllowed", + "WelcomeNotificationTemplate", + "ExpirationNotificationTemplate", + "ProcessByRatingEngine", + "SubscriptionStartDateAfterUpgrade", + "Published", + "VendorTimezone", + "MPN", + ], + "required": [ + "Name_EN", + "Description_EN", + "PlanCategory", + "ServiceTerms", + "BillingPeriodDuration", + "BillingPeriodType", + "PricePeriod", + "RecurringType", + ], + + }, + { + "pattern": "^(OpUnit_(?:[a-zA-Z]+(?:\\s+[a-zA-Z]+)*)|ResellerGroupName_\\d+|" + "UpgradePath_\\d+|SalesCategory_\\d+)$", + }, + ], + }, + "uniqueItems": True, + }, +} +PLAN_SWITCH_PATHS_SCHEMA = { + "PlanSwitchPaths": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "FromPlan", + "ToPlan", + "ImmediateSwitchAllowed", + "SubsStartDateAfterSwitch", + "SubsPeriodChange", + "UpsizeAllowed", + "DownsizeAllowed", + "PartialSwitchAllowed", + "RemoveSwitchPath", + ], + }, + "allOf": [ + {"contains": {"const": "FromPlan"}}, + {"contains": {"const": "ToPlan"}}, + {"contains": {"const": "ImmediateSwitchAllowed"}}, + {"contains": {"const": "SubsPeriodChange"}}, + {"contains": {"const": "UpsizeAllowed"}}, + {"contains": {"const": "DownsizeAllowed"}}, + ], + "additionalItems": False, + }, +} +PLAN_PERIODS_SCHEMA = { + "PlanPeriods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ServicePlan", + "Period", + "PeriodType", + "Trial", + "FullRefundPeriod", + "AfterRefundPeriod", + "CancellationType", + "CancellationFeeValue", + "MPN", + ], + }, + "allOf": [ + {"contains": {"const": "ServicePlan"}}, + {"contains": {"const": "Period"}}, + {"contains": {"const": "PeriodType"}}, + ], + "additionalItems": False, + }, +} +RESOURCES_RATES_SCHEMA = { + "ResourceRates": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ServicePlan", + "Resource", + "IncUnits", + "MinUnits", + "MaxUnits", + "Measurable", + "ShowInCP", + "ShowInStore", + "ShowZeroRecurringFeeInOrder", + "ShowZeroSetupFeeInOrder", + "ShowZeroOveruseFeeInOrder", + "SetupFeePerUnit", + "RecurringFeePerUnit", + "MPN", + ], + }, + "allOf": [ + {"contains": {"const": "ServicePlan"}}, + {"contains": {"const": "Resource"}}, + {"contains": {"const": "MinUnits"}}, + {"contains": {"const": "MaxUnits"}}, + ], + "additionalItems": False, + }, +} +SALES_CATEGORIES_SCHEMA = { + "SalesCategories": {**CATEGORIES_SCHEMA}, +} +RESOURCE_CATEGORIES_SCHEMA = { + "ResourceCategories": {**CATEGORIES_SCHEMA}, +} +DOWNSIZE_POLICIES_SCHEMA = { + "DownsizePolicies": {**POLICIES_SCHEMA}, +} +CANCELATION_POLICIES_SCHEMA = { + "CancelationPolicies": {**POLICIES_SCHEMA}, +} +NOTIFICATION_TEMPLATES = { + "NotificationTemplates": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "TemplateName", + "Language", + "MessageType", + "MessageCategory", + "Subject", + "ToAddr", + "ToName", + "BccAddr", + "FromAddr", + "FromName", + "PDFFileName", + "Active", + "Condition", + "VisibleToReseller", + "Security", + "HTML", + "PlainText", + ], + }, + "allOf": [ + {"contains": {"const": "TemplateName"}}, + {"contains": {"const": "Language"}}, + {"contains": {"const": "MessageCategory"}}, + {"contains": {"const": "Subject"}}, + {"contains": {"const": "ToAddr"}}, + {"contains": {"const": "ToName"}}, + {"contains": {"const": "FromAddr"}}, + {"contains": {"const": "FromName"}}, + {"contains": {"const": "HTML"}}, + {"contains": {"const": "PlainText"}}, + ], + "additionalItems": False, + }, +} +RESOURCE_DEPENDENCIES_SHCEMA = { + "ResourceDependencies": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ChildResource", + "ParentResource", + "DependenceKind", + "DependenceMultiplier", + "MPN", + ], + }, + "allOf": [ + {"contains": {"const": "ChildResource"}}, + {"contains": {"const": "ParentResource"}}, + {"contains": {"const": "DependenceKind"}}, + ], + "additionalItems": False, + }, +} +UPGRADE_RESOURCE_MAPPING_SCHEMA = { + "UpgradeResourceMapping": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(FromResource|ToResource_\\d+)$", + }, + "uniqueItems": True, + }, +} +TERMS_CONDITIONS_SCHEMA = { + "TermsConditions": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "Name", + "Acceptance", + "Active", + "Content", + ], + }, + "allOf": [ + {"contains": {"const": "Name"}}, + {"contains": {"const": "Content"}}, + ], + "additionalItems": False, + }, +} +OP_UNIT_SERVICE_PLANS_SHCEMA = { + "OpUnitServicePlans": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(OpUnit|ServicePlanName|TermsConditions_\\d+" + "|Published|ResellerGroupName_\\d+|MPN)$", + }, + "uniqueItems": True, + }, +} +REQUIRED_SHEETS = { + "required": ["Resources", "ServicePlans", "PlanPeriods", "ResourceRates"], +} +PPR_SCHEMA = { + "type": "object", + "properties": { + **RESOURCES_SCHEMA, + **SERVICE_PLANS_SCHEMA, + **PLAN_SWITCH_PATHS_SCHEMA, + **PLAN_PERIODS_SCHEMA, + **RESOURCES_RATES_SCHEMA, + **SALES_CATEGORIES_SCHEMA, + **RESOURCE_CATEGORIES_SCHEMA, + **DOWNSIZE_POLICIES_SCHEMA, + **CANCELATION_POLICIES_SCHEMA, + **NOTIFICATION_TEMPLATES, + **RESOURCE_DEPENDENCIES_SHCEMA, + **UPGRADE_RESOURCE_MAPPING_SCHEMA, + **TERMS_CONDITIONS_SCHEMA, + **OP_UNIT_SERVICE_PLANS_SHCEMA, + }, + **REQUIRED_SHEETS, +} diff --git a/connect_ext_ppr/service.py b/connect_ext_ppr/service.py index c4dc182..790af90 100644 --- a/connect_ext_ppr/service.py +++ b/connect_ext_ppr/service.py @@ -1,5 +1,12 @@ +from typing import Any, Dict + +import jsonschema +from connect.client import ClientError + from connect_ext_ppr.db import get_db_ctx_manager from connect_ext_ppr.models.deployment import Deployment +from connect_ext_ppr.utils import _parse_json_schema_error +from connect_ext_ppr.constants import PPR_SCHEMA def add_deployments(installation, listings, config, logger): @@ -40,3 +47,16 @@ def add_deployments(installation, listings, config, logger): db.expire_all() dep_ids = ', '.join([d.id for d in deployments]) logger.info(f"The following Deployments have been created: {dep_ids}.") + + +def validate_ppr_schema(dict_file: Dict[str, Any]): + try: + jsonschema.validate(dict_file, PPR_SCHEMA) + except jsonschema.ValidationError as ex: + error = ClientError( + message=ex.message, + status_code=400, + error_code='VAL_000', + errors=_parse_json_schema_error(ex), + ) + raise error diff --git a/connect_ext_ppr/utils.py b/connect_ext_ppr/utils.py index 00a3d07..3f1c15e 100644 --- a/connect_ext_ppr/utils.py +++ b/connect_ext_ppr/utils.py @@ -3,6 +3,8 @@ from connect.client import ClientError, ConnectClient from connect.client.rql import R from connect.eaas.core.logging import RequestLogger +from jsonschema.exceptions import _Error +import pandas as pd from connect_ext_ppr.schemas import DeploymentSchema @@ -105,3 +107,24 @@ def get_client_object(client, collection_name, obj_id): return getattr(client, collection_name)[obj_id].get() except ClientError as exc: raise _process_exc(exc) + + +def workbook_to_dict(wb: pd.ExcelFile, row_data=False): + dict_wb = {} + for sheet in wb.sheet_names: + df = wb.parse(sheet_name=sheet) + dict_wb[sheet] = process_worksheet(df, row_data) + return dict_wb + + +def process_worksheet(df: pd.DataFrame, row_data=False): + return df.to_dict(orient='list') if row_data else df.columns.to_list() + + +def _parse_json_schema_error(ex: _Error): + error_list = [ex.message] + if ex.context: + for sub_ex in ex.context: + sub_list = _parse_json_schema_error(sub_ex) + error_list.extend(sub_list) + return error_list diff --git a/poetry.lock b/poetry.lock index 831c512..0d2b1e3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -352,6 +352,17 @@ files = [ {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, ] +[[package]] +name = "et-xmlfile" +version = "1.1.0" +description = "An implementation of lxml.xmlfile for the standard library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"}, + {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"}, +] + [[package]] name = "exceptiongroup" version = "1.1.2" @@ -806,6 +817,77 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] + +[[package]] +name = "numpy" +version = "1.25.1" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d339465dff3eb33c701430bcb9c325b60354698340229e1dff97745e6b3efa"}, + {file = "numpy-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d736b75c3f2cb96843a5c7f8d8ccc414768d34b0a75f466c05f3a739b406f10b"}, + {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a90725800caeaa160732d6b31f3f843ebd45d6b5f3eec9e8cc287e30f2805bf"}, + {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c6c9261d21e617c6dc5eacba35cb68ec36bb72adcff0dee63f8fbc899362588"}, + {file = "numpy-1.25.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0def91f8af6ec4bb94c370e38c575855bf1d0be8a8fbfba42ef9c073faf2cf19"}, + {file = "numpy-1.25.1-cp310-cp310-win32.whl", hash = "sha256:fd67b306320dcadea700a8f79b9e671e607f8696e98ec255915c0c6d6b818503"}, + {file = "numpy-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1516db588987450b85595586605742879e50dcce923e8973f79529651545b57"}, + {file = "numpy-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b82655dd8efeea69dbf85d00fca40013d7f503212bc5259056244961268b66e"}, + {file = "numpy-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e8f6049c4878cb16960fbbfb22105e49d13d752d4d8371b55110941fb3b17800"}, + {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41a56b70e8139884eccb2f733c2f7378af06c82304959e174f8e7370af112e09"}, + {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5154b1a25ec796b1aee12ac1b22f414f94752c5f94832f14d8d6c9ac40bcca6"}, + {file = "numpy-1.25.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38eb6548bb91c421261b4805dc44def9ca1a6eef6444ce35ad1669c0f1a3fc5d"}, + {file = "numpy-1.25.1-cp311-cp311-win32.whl", hash = "sha256:791f409064d0a69dd20579345d852c59822c6aa087f23b07b1b4e28ff5880fcb"}, + {file = "numpy-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:c40571fe966393b212689aa17e32ed905924120737194b5d5c1b20b9ed0fb171"}, + {file = "numpy-1.25.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d7abcdd85aea3e6cdddb59af2350c7ab1ed764397f8eec97a038ad244d2d105"}, + {file = "numpy-1.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a180429394f81c7933634ae49b37b472d343cccb5bb0c4a575ac8bbc433722f"}, + {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d412c1697c3853c6fc3cb9751b4915859c7afe6a277c2bf00acf287d56c4e625"}, + {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20e1266411120a4f16fad8efa8e0454d21d00b8c7cee5b5ccad7565d95eb42dd"}, + {file = "numpy-1.25.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f76aebc3358ade9eacf9bc2bb8ae589863a4f911611694103af05346637df1b7"}, + {file = "numpy-1.25.1-cp39-cp39-win32.whl", hash = "sha256:247d3ffdd7775bdf191f848be8d49100495114c82c2bd134e8d5d075fb386a1c"}, + {file = "numpy-1.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:1d5d3c68e443c90b38fdf8ef40e60e2538a27548b39b12b73132456847f4b631"}, + {file = "numpy-1.25.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:35a9527c977b924042170a0887de727cd84ff179e478481404c5dc66b4170009"}, + {file = "numpy-1.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d3fe3dd0506a28493d82dc3cf254be8cd0d26f4008a417385cbf1ae95b54004"}, + {file = "numpy-1.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:012097b5b0d00a11070e8f2e261128c44157a8689f7dedcf35576e525893f4fe"}, + {file = "numpy-1.25.1.tar.gz", hash = "sha256:9a3a9f3a61480cc086117b426a8bd86869c213fc4072e606f01c4e4b66eb92bf"}, +] + [[package]] name = "oauthlib" version = "3.2.2" @@ -822,6 +904,20 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "openpyxl" +version = "3.1.2" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +optional = false +python-versions = ">=3.6" +files = [ + {file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"}, + {file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"}, +] + +[package.dependencies] +et-xmlfile = "*" + [[package]] name = "packaging" version = "23.1" @@ -833,6 +929,73 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] +[[package]] +name = "pandas" +version = "2.0.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, + {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, + {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, + {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, + {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, + {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, + {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, + {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, + {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, + {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, + {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, + {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, + {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, + {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, + {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, + {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, + {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, + {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.20.3", markers = "python_version < \"3.10\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] +aws = ["s3fs (>=2021.08.0)"] +clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] +compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] +computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2021.07.0)"] +gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] +hdf5 = ["tables (>=3.6.1)"] +html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] +mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] +spss = ["pyreadstat (>=1.1.2)"] +sql-other = ["SQLAlchemy (>=1.4.16)"] +test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.6.3)"] + [[package]] name = "pluggy" version = "1.2.0" @@ -1099,6 +1262,31 @@ pytest = ">=5.0" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + [[package]] name = "pyyaml" version = "6.0" @@ -1271,6 +1459,8 @@ description = "Database Abstraction Library" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ + {file = "SQLAlchemy-1.4.49-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6"}, + {file = "SQLAlchemy-1.4.49-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"}, @@ -1395,6 +1585,17 @@ files = [ {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + [[package]] name = "urllib3" version = "1.26.16" @@ -1424,4 +1625,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.8,<4" -content-hash = "656f05dbc86abd69b596a754af00415df7d50c7d0053e11461718dfdf53d68e9" +content-hash = "6295f5cb2941f991b8f7fd60d2fe49edc92829f3d38f91009ff3a935d6951337" diff --git a/pyproject.toml b/pyproject.toml index 03f780a..ceb5a9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ connect-eaas-core = ">=28.7,<29" requests-oauthlib= ">=1.3.1" sqlalchemy = "^1.3.12" psycopg2-binary = "^2.9.6" +pandas = "^2.0.3" +openpyxl = "^3.1.2" [tool.poetry.dev-dependencies] pytest = ">=6.1.2,<8" diff --git a/tests/conftest.py b/tests/conftest.py index cb646db..5bbd46a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,9 @@ # All rights reserved. # from contextlib import contextmanager +import json +import pandas as pd import pytest from connect.client import AsyncConnectClient, ConnectClient from sqlalchemy.orm import sessionmaker @@ -378,3 +380,14 @@ def api_client(test_client_factory, dbsession): get_db: lambda: dbsession, } yield client + + +@pytest.fixture +def ppr_workbook(): + return pd.ExcelFile('./tests/fixtures/test_PPR_file.xlsx') + + +@pytest.fixture +def ppr_valid_schema(): + with open('./tests/fixtures/ppr_valid_schema.json') as json_file: + return json.load(json_file) diff --git a/tests/fixtures/ppr_valid_schema.json b/tests/fixtures/ppr_valid_schema.json new file mode 100644 index 0000000..c291572 --- /dev/null +++ b/tests/fixtures/ppr_valid_schema.json @@ -0,0 +1,168 @@ +{ + "Resources": [ + "Name_EN", + "Description_EN", + "ResourceCategory", + "MPN", + "UOM", + "Measurable" + ], + "ServicePlans": [ + "OldName_1", + "Name_EN", + "Description_EN", + "PlanCategory", + "ServiceTerms", + "SalesCategory_1", + "BillingPeriodDuration", + "BillingPeriodType", + "AlignBillingOrderWithStatementDay", + "NewDayOfStatement", + "AlignSalesOrderWithStatementDay", + "AllowScheduledChanges", + "BillingPeriodAlignment", + "CotermingPossibilities", + "ExpirationDateAlignedWithEndOfMonth", + "ExpirationDateAlignedWithSubscription", + "FirstBillingPeriodForFree", + "PricePeriod", + "RecurringType", + "AutoRenew", + "RenewOrderInterval", + "AutoRenewPlan", + "AutoRenewPeriod", + "AutoRenewPeriodType", + "BillingAlignmentResellerRedefineAllowed", + "WelcomeNotificationTemplate", + "ExpirationNotificationTemplate", + "ProcessByRatingEngine", + "UpgradePath_1", + "SubscriptionStartDateAfterUpgrade", + "OpUnit_DE", + "ResellerGroupName_1", + "Published", "VendorTimezone", + "MPN" + ], + "PlanSwitchPaths": [ + "FromPlan", + "ToPlan", + "ImmediateSwitchAllowed", + "SubsStartDateAfterSwitch", + "SubsPeriodChange", + "UpsizeAllowed", + "DownsizeAllowed", + "PartialSwitchAllowed", + "RemoveSwitchPath" + ], + "PlanPeriods": [ + "ServicePlan", + "Period", + "PeriodType", + "Trial", + "FullRefundPeriod", + "AfterRefundPeriod", + "CancellationType", + "CancellationFeeValue", + "MPN" + ], + "ResourceRates": [ + "ServicePlan", + "Resource", + "IncUnits", + "MinUnits", + "MaxUnits", + "Measurable", + "ShowInCP", + "ShowInStore", + "ShowZeroRecurringFeeInOrder", + "ShowZeroSetupFeeInOrder", + "ShowZeroOveruseFeeInOrder", + "SetupFeePerUnit", + "RecurringFeePerUnit", + "MPN" + ], + "SalesCategories": [ + "Name_EN", + "Description_EN", + "ExpandCategory", + "DisplayInCCP", + "ParentCategory", + "Name_DE", + "Description_DE" + ], + "ResourceCategories": [ + "Name_EN", + "Description_EN", + "ExpandCategory", + "DisplayInCCP", + "ParentCategory", + "Name_DE", + "Description_DE" + ], + "DownsizePolicies": [ + "ServicePlan", + "Period", + "PeriodType", + "Trial", + "Action", + "ActionPeriod", + "ActionPeriodTimezone", + "ApplicableTo" + ], + "CancelationPolicies": [ + "ServicePlan", + "Period", + "PeriodType", + "Trial", + "Action", + "ActionPeriod", + "ActionPeriodTimezone", + "ApplicableTo" + ], + "NotificationTemplates": [ + "TemplateName", + "Language", + "MessageType", + "MessageCategory", + "Subject", + "ToAddr", + "ToName", + "BccAddr", + "FromAddr", + "FromName", + "PDFFileName", + "Active", + "Condition", + "VisibleToReseller", + "Security", + "HTML", + "PlainText" + ], + "ResourceDependencies": [ + "ChildResource", + "ParentResource", + "DependenceKind", + "DependenceMultiplier", + "MPN" + ], + "UpgradeResourceMapping": [ + "FromResource", + "ToResource_1" + ], + "TermsConditions": [ + "Name", + "Acceptance", + "Active", + "Content" + ], + "OpUnitServicePlans": [ + "OpUnit", + "ServicePlanName", + "TermsConditions_1", + "TermsConditions_2", + "TermsConditions_3", + "Published", + "ResellerGroupName_1", + "MPN" + ] +} \ No newline at end of file diff --git a/tests/fixtures/test_PPR_file.xlsx b/tests/fixtures/test_PPR_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..de0cf19d9cfd56551701dad105dc7827b794f5a0 GIT binary patch literal 87657 zcmeFZc_5W-*Ef7_V=^S7Op$pe^N=a?T#;EQLo$X)#!?b8&vP;)W5zO_rJYK;pVYVP4}&VVY0*4B!%xI)FtrZh%Z$g9pUvW z%wP4|AbeYk)%O?QMIObo8kzIf-+K8-f8|AXa5=Uo)yu1ma;)uD*Iu{?+kK74EwUIh zUoj80ktddqC~NLfpT^&uGl`hfoIH^>!FAZK>3YnehV!(P4G-)S=wDrrJJg^{OF8zy z-hlqq`2`JxqM1C$h29GRnu}duY6LERk;LRVxwUyiCe~;(Pay!yzMG8Be{<9z8(Iw& zdaCL|FniIB7<;VYy!}bxRIDD)!^y4%+0(A1cOIpkkZ_M{R!fhvCAh#n! zUz??Zpsg)*=-i&*F1g4}w*U~U0%8{z1h5+y z!RoS|ofTXIcVY$s3L$v*fA@ctQOegqA&^|UH}LYRQHui+6U(zAmV4HH3(%{RUVa>8 z4_D`=*bMBi9qQx^6z~mg|2}6k?B_S__{Kkvf5s8LHO$34!1iG zHLx=nZX|wPjULFaF~}$Lia))War+M6%n#*LJcE4m_KHLaM$ZD%3V1BeyhQVLHyI^k z5^9dV>qjqO)8gM6+UyF0NRPXkG*Ll;f&y!)lF)-^(EL2Z+n;*+M3+)IU#{mi z28J+@J&AcAihV3&!&g(;Z)DRsU#78h9bC#kSMiu`6boQ^`;e1H!kel2~%`A zC5IF77i|yw$=bdfmphg+sChL|-Eq{5knBE>2;*IgiC2qj*Pql9Pj{Op9?E;U6hA~t zFnKh8XxW?od}P1iPxrWd%SMldRgOcIYMoO{_+?Z0c=_Wu>y5wSJzO;Y`YV31v9Nzg zRH)8S!t^;|i$B?g8`>`3SWL<;H#PEUTDYzj3y#EJ^uSvYztv?mm|jra~{IX*Y_H1IggkHq=gvt(ua^OHOq6@G>@93lJEPi5ef$e2!=^B96hgX9{^ z*<9jidzGK36xdH>VqZSD>8xWmnvC18t4PzQR_K9uqJW=%M8fpdW%84>&c}pjbZq@P z3w}6R$UIV=p=r63LGIRuaj#m}xrs%Q=X>;h3j)(V>hfQYpJQRf-NNYhkhLk zDmK&l<#m}KbuWCB@S|}fud{lz<9KD(D1!8=)*QJh~&sFTF#e0==}QQhKd=*p@Lf81ma%vvy>9e6AxSIghCh|(&+KH zx>*p+l*!fHUa#-=^_jHNkXqrUZ&$m0X0&%~M5^&(ZRgDy8vRRajs|CBBGS{J9wq5g zef&@y8)x|u#(YY#rp^+6<}YEL5KoJOpNB0z7v#3Rye)IlzN_^~3RYRBcvjb4Cn)H? zy<%8?p`lZVSu`fLEm_%%?}l|0(@e)!yHdX4=a@GLB%CIvAo!tEZ#*O-$96(c-{u&e zeC{*r-k;caYeLD#s4I`(GG|e>cg+m#PIs7W8G9brullZx_#zMWXZ!X0l3I6^R%-;r z&*BF(-HUH%J8nxkFsM4i`Pof0uDUvqtN(Ki1;$3p(h~K_RWCJpuP(yTUhg>jkLLs@ z!UZP=?^byoSb_W8Td(nf>brRsf=>LC_Z+X9nK?OfA>a7mBAgJ>64Xa?%y%hrmB7y< zkc*7z*_XhlWU_Dbp5D?W4!m-Vn=>T-X|-GMvqYKZda{<$GAZ{l3H#ZfzjEkB9Io8U zbEL#PC0A))PWX$nLBcDC$uThJ@eQf;Jl7zu&tJMfjH$PT3%KcDpC%-{7C+Mbf|2fZ zp+DO-^!{0LIVGE8H)buJUkmtlzEZys_Sz(dNLKct=vni6ZYSNt^@IGP%$F(%EVm>?pxCI z3^DJuqB$X+@iP(`H8i+qdm|TOwwP7;7<{Q^D{c#FuAf$Z-x5hOXxxNtGEVZ5GrEZ& zwQ+8+poTH!eBa%0Zp-+J3F=E6xCqhD=J)Wp9M>6|1&R)Zf9N>g&h=7#ahj^+*Ygjz z1_`Wb)D9hLVX&|WcgGLbc4rFc!?e=^k3w_7G3;38z0X zWAa`d+rv}XFTZ#T;AIQ-64BRZd$ITEyL5&QMbm9I;E<+FU6co2gkH zud9pqqI{QJv%b*n)!aSCW#qNCI5#|H6m%pxsrqxO#9$$p(OiFW%>;GwK#kk2^^wlb zk!UY@PgjPY)$$+NLXU{i2D;zuqgz$_+D^Bq^tFd>Q|W6L-JH_b&vZ?V!GmAt*2dNc zkE{tT7ufX%AIZGmu;pGXnnPuZ-T%^gmg@3-_V)4wEJNGgk7oBvd?r&wdkCtfCqppx zqUTduIQq&b3!Fo!oUr>|y7*BQN=`VCIlVB`kEZ%aQ0OylEV@kaOq%qnVV)0X*<{20 zk>DDxKbEefIOzF-Bjuk9b6W_kuzRIk3aH%g_ng8l<{YHHwY7vk8)akYtRvb;Q0_A| zB>I{_H|2(KB~^qgPMJUm=25@1cwa)iO(O@c6`!9zGV;@7sG#M}A(>NH*A*lSmF)P* z(C@ElJTLuZc8ZSh-ig;@jTX z!@I6n-AJ$wh_>$|-E)jWj-Vxm^INLbEobFu4xjduMhm-mEBpPU+tFA_e(G9a*39`h9P9zg_3Gx&FHJ zzrIpw7qFV>vDZ?6NET~lHkIDQDYHT765eKhO+$dApf#S$*!Sgb1*WMKMk=9?5Iv1< zdRd%x0P*zR(=LdhyTBL!UAaOIwVu5z9eCxrz9gy4KUQoi73wdO)m7PL@Q**v^pbK7 zmva1Jt%U1kf7+(LAFYsP7c@%WkoEmvTyeqO<#RjW8Vl<`uZ*pgGPkk)`Pk=wty8g9 zG;H;yXtpJ(@9=^gZ5NBhHLcU)+98E4>HAqVF^J994CoKJf7Uq7~A zaF09mvL{USEAOtUOAMC12~O_T+Z|!Oq}r9lxBr0a8_}bG*hqVyjSTkt?EtZT9?<@` zuUP%EE#$cLYRdLc&{OS!xLbAu^_K3E(LZHFCFCXOX7PND-YgIFD3aX{!B8?Os8e%8 zYm2(VD|)!F`S6vQ>NMqwahmaNsV$~oqpy}JiP3S;BpNmPtBOmGY5(ASkN;D7=DLnm z6@y261II>yo$w;&gTWr|Nw+u;bv4KFyaxunjkT3dN+c9C@fW^b#lgLm$8LUaKpyug zJ4SsZR4be%*PWYs)rZ$Qu75PJD1h?1Crz@JDgWXR#Ir@O=!YAZ8E^9Qy?Nn>k%XO< zt##O6G!N0ud zUzrv^l4*sj+V#OxA@oIRjDF(Yppjg;gv(avbD~X{OBZt*o!Dilljz6L>o(VzEv}<| zCDJUAjk|L)>|^$ujZ18Wm}BYuB&wM>490hJ({;5My4y0FER@`bGDe%ppL|E0sWS3h zFX+)No8yqAQQ)plwK-(i6my*8>U3r)x)0mK8*$j{PFAC$*p(**EZ-E;v&4*cw9`Fu zD&w~9R6qo4Es(1SouyQR?{4V*b-sd}=pKVrcX>+CtpM@#;)qr1QON zN=jpNY@&7MGj~bo*f%+~7Yp;gCx6|FNl_h%PzgJuc6@}(kn`FDCdvw0f;fLq#g>_s zgu-u^3EQ*pCf3X0Ko)zh`1{_#8f!7P-j1i_)YCn0BXh{7A#@ zSwGTwLK=6^)vjv14^tioe zpPl32!|i`nz>WFDS3HN$>2S91UvKCA>)W~i`gZ=myj?mG=|9l%l+MrRrh>PO!W~nE z+nNeKPL19wEae3yR)O=}Bwn8d*a*IyLJ*z82|I;(;S_$yDRiFn@2{d5mz?2XQ)evAOtdI@ zP7=`^4Wl8uKts|&L&Za*;m>JpS!EY9Lx?>W0zC-9XbM5g3c(r=LBw8gGEu(bSr>O- z$nQRe!d-d=-(w2*_!RErHAbZ3>VI17bKPprOe47;xbN=aJ}!=pvSPi}gBMly3Qj+A zo$#RJFD&W&cZ9~$xi}xv9BA&S&6CR1UuX_G_V~=Rg;W2&P@A$*HMY&0QzHjB_iwwg zP8Jj|40c=_KiEAS8+ESZ0h#m%d$vQOr!DF8-z0rDn487^CODKN{c1m2 z{nzz;HbPns((q!>b<6RZ^bZ{eBTR2q{&VMFZ?StE7<}pJUa$YY-oGj2z&ZJ&!OL$0 z|93jy%Tq)TpTdcdvghpU1~H7^|sY(aQ;rjZ*`lgLsem~>hTvbRvaKDrp^5yy%7RK#14%lh2kvvNDn4CU+d42le0 zEK_8Q${ZK#G$6l4b8MR2H`j{A*!+#24-U2;`Wy7o8H(OjI@JIx`?!h^$Vs-=dBL2 z4Q-P`(NSZ%5zM4>D@zxe<;2(|UiuE+?O!UmX^^MW@-!`Zd`qBKI6zuNLH@dsm64N3 z7X5&Ij=#_o<<$t18s6rNa5V`pdh3neI2mEi97EX#Twle|A*tJbuV^SU@z3~NIP|J{ zeGr|OOMJBG%mv+GF(Yc}R#B2IC)1Z=XY_w@X15PHzU;mr^|qTeA#lX>Olzk}RPe1a zu8IwWLodeXJ0I0}WlNH8A7{R-BJeCovR?Z^zxs;VqMp70giVYAY}0_`h5&VEqx|FL-JX~K2Vo(k4nQL{5z6? zMT#{nUO9QaxoaETkG6Sy?Vk`VWKmF2|E0>V)Yi(T)u^b-_Us~0JFoTy=4O>P?`O(C zacfgElnyJdFfgpV;Jzex?XuFfqka}SX(3U`Mrb47`+C1ej~zEVZjqJ|)dPQuxc|dQ z%=hOlN&xV${vG&&dw|dOOM+(lbZMvD1Cq?An|D%MW->y|CzBk6gX>LHt#F~{+*P80Xtky5uw97TBb z`>%XN`JJv*Z+ui|L=J@njj{S&2m>E16$DDW96PZ1nD5>sPmI-$y8CvRza`r_B%Q2c^{-_!Us&1e_!pI`MFx7Jgv9M&PZelB`} zg>8VR*}PLtVur|h^Rj9xW%S3q$0HkbWozm0W$K{Qnu z&mGg?5aU$zvkoy1G79a=4@1y05(++G1_;xcF9$r@EL}gp9z69w33Vpi5fNfG2;x#f z{B7zlbP&|T^|DAZ^-BEX`mU8wTij_lefY%j#%QgzK^K~b#4K)p{?=GNqp>cnt&=M$ zB)zw9bU2Z`4T~W9emBdkEbn_8b-?=wqA^!(M>CNp-;YvyBuTi(PCe{@PFhP$@06FC zyX4UnLkwC`qE6}iOb?&Mxvdvljo!ko3{r!!j+^mf^815xpWIGlSf+Vv)0L8oc-_%C z_xZ7Bck@7^rQT8f=Ze@(#EEt`wrnWx$E(tpOI;Xy^ znc;hh@rDzR-oAadK-t2Yu!{y=0WWGc(fN3szYX&26(X)PW=vY`BGz%W;lxiRu| zynVrINfsVTX2M5&hvbWVj-Pv)+H#t4Y=BN6=V%kw=&#jQqt~>X=1u;hH&6d4%kc>4 zzw`EjbX{4qkw9jXQ$pqlyB8%vP5cIEMkn55x?E|B_%%n8P)jbCuGmEr_{tmfqS9QK zaehh{uW3|PD)?UxBSmnOzN7FUCAx5e^4XQgbf1c@++w(f`Hsu9{inp?0M<=p%}3_H zh{S%|QMoV~qc`8Nbp#cE4b}=2TqbGi=B~sVy=Hz|z}#ifQ<~mFgg5B0@c4oGP_l=*O!CUL!GO4{PJf|B_fT>kgFrusNjHAYDf-TW&FypmBjtd<3|eb@P!Z*-kzk9 zr|*4KJdvM%ytMPHDW)Y2l_`3!RFFcUwAo9wo&p*+1@v(mibetlnJ_BWc;{D7F?FTL z6|j3HokQrtCeP`?#Jlr8E(uh^)Lm6s%zj->bz+Fl!1gk?zAcX3xAHAq%)a+dj zygeHhK4i+d5<-%8+bIO!6kC|Ki6FA%j0u-d4h?>qk2Q^G<55NDCzE41xa7N33ac~d z?Hse;#F?C`qtiXzxogj)q-U9X_Kff(994g{em+J5cmBS~CSNP{{gwII@7P~n6lS&! zud@$ZTyFhsfE7wsw_Ei|SbI*^+sV7gxA$a8N%>-8_v247v6=5OL2u`LN#U{sQ$v<|N9Tpedr^deznZ8l^ZBxpq<|Og^UYmrJl6y?HxZv>Tqf|i27XoHZ z_Jx--{;^wn{Dc*oJT}wBm)*t`t1@sC5fS>sNSE@&sHhThVs<-OovWvFuan(wz1(LC z+7 zvHEVPIV@|!!20y3Ay$d^ooDl|82(-)XG)mtSq$KITz#KS|8q;x-*1Z)^B~CFuh*4* zdbRD>Yd^?01OEq?a#+4_u<7?Zd-Kjdz5ehcZ2AMx^&eY`{(k{+1I$vbMUP|=Qh6SoC)`yx>Dl*c9XR*(Z)0GN`nz}17?OmyKb>XO0 z;sGlSWCr)l$b`+YGzoi6=>){#9d;B28zEv<%;wdp zpRGQiAJN!R6ljaX+|u>*-E`@*7|2`mSUm=m_LQ3uqKUkB8X;rKla68E9mGIp^ghf4 z4GO~}hC)nZAX=W-3uw>R8o~neuQqWbAVpssNP$NMIvT6Og7_?Js#vdG3?B2Qufu@o z$r!!;R5BuSC9}o%IhbEspd$ z##*gph%3q8YNgBy?Fc=|EB_d%#3Xh0n_6V*XrI_bZ;||@MI!`4F^_IgAp|j z!#2g8C;e9GxUE6}k0fiBIU009l^9keV+J4Xr2)qL$8sLBw$l2_nkz(^t@-jw>)M8?0lXcXx`% zf@5b!y2ovG0^G1*(4o-O%8f`+wQMtUr&itu<3HCU|8l(_T)&BJADy4x5a;&&65^I9SOj4ujvZNP*=t#b443wjGgZ6t8Vt<^63iY9;A5}u-Sm*0u%C8p3N6$Xj0pavrg2zB89`V4aAKLP=A;T1Qqy~ z#rsc=WP=U)o&8-=U_)-86$qb%a$Z;FsYI`hILQmX%|3p5gbKOdG&KFAbRp(6!dQt32&ILC`0jvF(A&JY z4z{1xD}zrQ(*Xx1YyjFqI4UbzIbaWFpsWE0Z6Yvz1nakqiVqyk(D)Jh)8KJ+-g{V3lbU&qQJ%W@*HRv@^TXaPIodOOBHS{TKNNf zClEBH&bxwU)TT~+E9u4J_0^>7Y1+IXVZ&JgC5en&V}}0MY9h2v2Ku6jyHahs3!+C= z@L58T0vw(@Srw%J2qH-N2V-g1Q8=dmNdnD0>4oJFY;MS`&fnSWb6Z`z6SRPhMK#Wa zC(@C8!YQrh^a&t4fN5;g|9SwmT@qgQgb)MSmShu1?RFCjbJ=8T6O5!iM-}UjzW@Q_ z&ET962E+WDJq+_2HS0n;7-TJzLg~PHCl-5EEtEeyl5L$b1nvg88V0jioha;~S%RUQ z05{H&H65Oy1+*bJY;wv*BEYJ~J@$f$RoC0hVjvv{7ms(#!HEDvwMn9Cz+1*THv|(g z9pLRIP>BGjanB_IY+G9}Zz~f2HK}D;JPK(iph^4;(4W!UWToFpt6401&VwCQ5g8$^ zq3cRu&pOh|EKMI5)l9E4^)!ihEf}?|XVk`L=6MhW_*|;r(p{rk#l*=-zo=Tjrt3&1 zH%BvOIhnc8?HddER^9CE6<9I=VUkl{A#UNxe?|CYrV}`*vBae~fXseG0j=F-`IDir zrTASFVA$ft|I8@znqFyIR{xsc&D6McC&~ugNiH6xf^g@B(rd1XRiX<8d{1iOMilVd zUA8$HYI+m9cJz#zqB+P50v%$}CTPYU1FtvF&^FByc$3@$4uDOW0b&@V_GHxq*;_Jj z_*N_kF~km2)%KnT8UhR5BBuD|M-NON^#hz6gnLoW^b6QPzZkV9j>1uI-8~Wk$9(#o zst5RPGOL}7DiC|`NW^Bj7%}9w*r7SVYvIv95^nNg;B}r@s$3T++7q5kB1CYqg+!Ma zqo$GTO!k9ymN{z+ZrO?8PJu))1NBq`H&R;k)HJXp;tgUNV;aHs0Rcf;{ea(HMue;y zx0X>{Z5$~UXeYup4d)MNZ8@*ak_qUn^>YrL0qAVPHt6Ksk2JyQ!C%6vK-fi8RASkM1!(vH?UvZZI`7c*nzC%P7K{ zfej(L1~!A8XG1M<4!oZbSW->@QEeqMh$yHG)_C}Y*hWV5edsw)OYwlYlgW6>>x`w# z$OE(g1P8u}%{}=u{+(~-7`#Rz&~E-p{aFccCg47`8a^{7P~wbp2VY;3piP98n}Tft zl$-MG?2TPohabCBGvW9NfTUmq)()Wd?VwZC81lHC$m8O8n|ZBE!Z2Ff%qWsY>dm$V z>#YJ=Z@`8C@v$zLc;glvJUtsO9%>*AjW^&E0jOiuj!%9-nuw?tHW6?uv~n9TpA%XL zHH()6jv#lIA}%F&g4C*Ws1XRM##A+mK$jACNb#s9+01B`sC-FN_Q$eYn^eZRZWr> z_8W1C2C$|vlW`m%=M@mIDB=N}xzr@#%;lZvZP{$f>T>Yq8MIcgFq}L^5yBv4LC$Cy zETFo9_jfRDFlM3wET8%bDE{6iOhfLZlVYBh)f3CFYA~OROT9CX>BuHSF=6k8YXZOt zFS>AUs*WXFO#4>Pc#6PPv5Jp(r)R(g7fIxKVMDZ*^o8#(c7XHGy@@ubse<6z zkLeAw6caQ*z*1cx7b>H$xm*qq&k*Q^ev=|0+{Kd(u@ymFP0RjKz_4(efTT=EK?x_S zHEf6m7r4`B$SQGBh}kv(*a_6UQ-*Nnp@Cx*G!1}%6{=(r+XIWI;)27xg!2!MI8(c2!Jo5=VW{IuiUIt*@L z$hJFGV?5QtM){~HfPvtEEL3W>NSF3qanAjI?!zk<5FD2v%5ltEm=I&q(6}L(86due zann4HgdLOaA|Kwq;!Or@BKwH95_I)@cxy@OZ)+p_IZ*Eb$yci-Q^+^pbgV!oF&M=l z(QfN8b{y91>Cqdh53hnE(98s51E8Ro2!-`Mc5p@k4K@NQA=aW2;`52+%k3Cppe6u9 zJ*p8lTmebmYJC*}Q!Ic_`Vqk;-a zRrl@S$Qb+?HF%QZRaM3rOc@elJ!-)(P;LKf2 zA5Q;U4-7!{Lh&@DC7OVRcXl$Rv_#s43xdZAdU2KLe#|v|(ISX3VDn?+ME{frBn91b zAfzQgMFacKU8zSUm>t-UP=j1C3A#-P-y?r_%B%-fs}AEFfE6(G3BKDX<|t+k_g4T9 zc*K$*AmyZ!)nFQc@%6Vy@k9}07np|l6TMu6Kss|0xP^R|4l9{wffOzB6cWUV@thkov|6hHSqp^SB=eJ0P)vybS{c%qd`84?gKe z2C0}iJkde6958rL3sU^r=^T(e7EZDYoXAv$gf$ZIum=Gr{H-(SBazJew?~x{z2|>N z!tYthZ&QPk1tR2+>*qf$DWx5UYFhwHy#|=;^;7AlR2Uvq+G*}n( zRof+FnG;nb0e#!{v<RW^DeNqCB zR5;+^_nq?yka^8q;(}lYfVoE%1n0ymK5SDi2oQ=DdT!l=xxoao){Bu=(J zE27}5g-oCSL>vk@s6Kcn89_*v4{G~n(u;{@*lXPI`M0}r@UI9|xWNGp-~)H*+d&V< z0w_-ZQzCQX184@NCa-v|}KeFdJ!7zN!jDLlJ=R!Bh-z$@6GB@7`5q z_D;qLfJN0%&%$()jKTZcHzn7@K_f*KdWsnHRn=Do8^$*_NQ9Zuy3JP$ITL-$%g-%BpX&6wMi6*8(O9zG%%D!PFo9zxm9CTzAZtdpdOC9 zfkDHzMu3g*nZZ+t6>x;lAbY2i{hqaTETO6;a(E`VzyNn0+k}sv7n#<-<&6ZuqpMgh zhux{o9>kgiurfG2a)wlF0h$unXQ*NY8pGQD_Gq}RK+?_A;EC zK`vazR@xyM@CF7wI0|X$<#nr-P|=D*^Xd%gGLP1DD5`D4h;k@JIM=N2akzj@WJ%AU}^pINCo5~ZM1$Ao;xDB zIE+nDgOQEo0E`55Cc$+Op_?w0LnLeuRp6Y?g9MjM%xYGoKz#SUDJvs+p zzpQ$XmNpY>C7U_3Gb#tv1AMmQs-M|bW0ChQ9@!Hh$LGIilJGsVom;zl1~7Jn`|Z(B z4Zp{Sy9SvkZezfwkdd8yFz0 zqXDc>5T{A>{l@<;xjxX-eFR4M-7mxZ0aaACKj;Dc1le?rV;UnYzOuM}8^@PX~xY61QnEV

G-;u55 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 SL7__oR}EezDS_Mp_rCz)$A{_w literal 0 HcmV?d00001 diff --git a/tests/test_service.py b/tests/test_service.py index 1bb6c84..ad1e2a9 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -1,5 +1,8 @@ +import pytest +from connect.client import ClientError + from connect_ext_ppr.models.deployment import Deployment -from connect_ext_ppr.service import add_deployments +from connect_ext_ppr.service import add_deployments, validate_ppr_schema def test_add_deployments( @@ -39,3 +42,76 @@ def test_nothing_to_create( assert count_before == 1 assert count_before == count_after + + +def test_valid_schema(ppr_valid_schema): + assert validate_ppr_schema(ppr_valid_schema) is None + + +@pytest.mark.parametrize( + 'required_sheet', + ('Resources', 'ServicePlans', 'PlanPeriods', 'ResourceRates'), +) +def test_required_sheet_not_present(ppr_valid_schema, required_sheet): + ppr_valid_schema.pop(required_sheet) + with pytest.raises(ClientError) as ex: + validate_ppr_schema(ppr_valid_schema) + assert ex.value.message == f"'{required_sheet}' is a required property" + assert ex.value.errors == [f"'{required_sheet}' is a required property"] + + +@pytest.mark.parametrize( + 'required_column', + ('Name_EN', 'Description_EN', 'ResourceCategory', 'MPN', 'UOM', 'Measurable'), +) +def test_required_column_not_present(ppr_valid_schema, required_column): + ppr_valid_schema['Resources'].remove(required_column) + with pytest.raises(ClientError) as ex: + validate_ppr_schema(ppr_valid_schema) + assert ex.value.message == ( + f"{ppr_valid_schema['Resources']}" + f" does not contain items matching the given schema" + ) + + +def test_extra_field_not_allowed(ppr_valid_schema): + ppr_valid_schema['Resources'].append('FooBar') + with pytest.raises(ClientError) as ex: + validate_ppr_schema(ppr_valid_schema) + + assert ex.value.message == ( + "'FooBar' is not one of ['Name_EN', 'Description_EN', " + "'ResourceCategory', 'MPN', 'UOM', 'Measurable']" + ) + + +@pytest.mark.parametrize( + 'not_allow', + ('OpUnit_123', 'ResellerGroupName_US'), +) +def test_not_extra_field_not_matching_pattern_allowed(ppr_valid_schema, not_allow): + ppr_valid_schema['ServicePlans'].append(not_allow) + with pytest.raises(ClientError) as ex: + validate_ppr_schema(ppr_valid_schema) + print(ex.value.errors) + assert ex.value.message == f"'{not_allow}' is not valid under any of the given schemas" + assert ex.value.errors == [ + f"'{not_allow}' is not valid under any of the given schemas", + ( + f"'{not_allow}' is not one of ['OldName_1', 'Name_EN', 'Description_EN', " + f"'PlanCategory', 'ServiceTerms', 'BillingPeriodDuration', 'BillingPeriodType', " + f"'AlignBillingOrderWithStatementDay', 'NewDayOfStatement', " + f"'AlignSalesOrderWithStatementDay', 'AllowScheduledChanges', 'BillingPeriodAlignment'," + f" 'CotermingPossibilities', 'ExpirationDateAlignedWithEndOfMonth', " + f"'ExpirationDateAlignedWithSubscription', 'FirstBillingPeriodForFree', 'PricePeriod', " + f"'RecurringType', 'AutoRenew', 'RenewOrderInterval', 'AutoRenewPlan', 'AutoRenewPeriod" + f"', 'AutoRenewPeriodType', 'BillingAlignmentResellerRedefineAllowed', " + f"'WelcomeNotificationTemplate', 'ExpirationNotificationTemplate', " + f"'ProcessByRatingEngine', 'SubscriptionStartDateAfterUpgrade', " + f"'Published', 'VendorTimezone', 'MPN']" + ), + ( + f"'{not_allow}' does not match '^(OpUnit_(?:[a-zA-Z]+(?:\\\\s+[a-zA-Z]+)*)" + f"|ResellerGroupName_\\\\d+|UpgradePath_\\\\d+|SalesCategory_\\\\d+)$'" + ), + ] diff --git a/tests/test_utils.py b/tests/test_utils.py index 777353d..aeb7a06 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,14 +1,18 @@ import copy +from jsonschema import ValidationError +import pandas as pd import pytest from connect.client import ClientError from connect.client.rql import R from connect_ext_ppr.utils import ( + _parse_json_schema_error, _process_exc, filter_object_list_by_id, get_all_info, get_marketplaces, + workbook_to_dict, ) @@ -77,3 +81,64 @@ def test_filter_key_error(marketplace): with pytest.raises(KeyError) as ex: filter_object_list_by_id(mkp_list, 'MP-XXXX') assert str(ex.value).startswith("'MP-XXXX'") + + +def test_workbook_to_dict_only_column_names(ppr_workbook): + dict_wb = workbook_to_dict(ppr_workbook) + + assert isinstance(dict_wb, dict) + + for key in dict_wb: + assert isinstance(dict_wb[key], list) + + +def test_workbook_to_dict_full_data(ppr_workbook, mocker): + data = [ + 'Dynamo XXX Customer Voice XXX', + 'User license for Dynamo XXX', + 'Capsule corp Commercial', + 'XXXXXXXXXX:0000', + 'Licenses', + False, + ] + resources = ppr_workbook.parse('Resources') + for col_name, value in zip(resources.columns, data): + setattr(resources, col_name, pd.Series(value)) + + mocker.patch( + 'connect_ext_ppr.utils.process_worksheet', + return_value=resources.to_dict(orient='list'), + ) + + dict_wb = workbook_to_dict(ppr_workbook, row_data=True) + + for key in dict_wb: + assert isinstance(dict_wb[key], dict) + for col in dict_wb[key]: + assert isinstance(dict_wb[key][col], list) + + assert dict_wb['Resources'] == { + 'Name_EN': ['Dynamo XXX Customer Voice XXX'], + 'Description_EN': ['User license for Dynamo XXX'], + 'ResourceCategory': ['Capsule corp Commercial'], + 'MPN': ['XXXXXXXXXX:0000'], + 'UOM': ['Licenses'], + 'Measurable': [False], + } + + +def test_parse_validation_error(): + error = ValidationError( + message='some', + context=[ + ValidationError(message='really'), + ValidationError(message='nested', context=[ + ValidationError(message='error', context=[ + ValidationError(message='to'), + ValidationError(message='parse'), + ]), + ]), + ]) + assert _parse_json_schema_error(error) == [ + 'some', 'really', 'nested', 'error', 'to', 'parse', + ]