diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ce7aa94 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ONSdigital/DFTS \ No newline at end of file diff --git a/.version b/.version index e0ea44c..afaf360 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.6.5 \ No newline at end of file +1.0.0 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c639ec3..8c6d1b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ -FROM europe-west2-docker.pkg.dev/ons-sdx-ci/sdx-apps/sdx-gcp:1.4.4 +FROM europe-west2-docker.pkg.dev/ons-sdx-ci/sdx-apps/sdx-gcp:1.4.5 + COPY . /app WORKDIR /app + +# Export dependencies to requirements.txt and install them +RUN poetry export -f requirements.txt --output requirements.txt RUN pip install -r requirements.txt + EXPOSE 5000 -CMD ["python", "./run.py"] +CMD ["python", "./run.py"] \ No newline at end of file diff --git a/Makefile b/Makefile index b11d335..f1037d4 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,6 @@ SHELL := bash .PHONY: test test: - . venv/bin/activate \ - && python3 --version \ - && python3 -m pip install --upgrade pip \ - && pip install -r requirements.txt \ - && pip install -r test-requirements.txt \ - && flake8 . --count --statistics \ - && pytest -v --cov-report term-missing --disable-warnings --cov=app tests/ + poetry install \ + && poetry run flake8 . --count --statistics \ + && poetry run pytest -v --cov-report term-missing --disable-warnings --cov=app tests/ diff --git a/README.md b/README.md index 60c11de..cf7bf4b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,43 @@ # SDX-Transformer -The sdx-transformer service provides data transformation functionality for SDX. It is used to transform both survey submission data into pck files on the journey downstream, and for transforming the data headed upstream for pre-population. +The sdx-transformer service provides data transformation functionality for SDX. +It is used to transform both survey submission data into pck files on the journey downstream, and for transforming the data headed upstream for pre-population. + +## Getting Started + +### 1. Install Poetry: + - This project uses Poetry for dependency management. Ensure Poetry is installed on your system. + - If you have poetry installed, you can skip this step. + - If Poetry is not installed, you can install it using: +```bash +brew install poetry +``` +or +```bash +brew install pipx +pipx install poetry +``` +- Use the official Poetry installation guide for other installation methods: https://python-poetry.org/docs/#installation +- Verify the installation by using the following command: +```bash +poetry --version +``` + +### 2. Install Dependencies: + - Install the project dependencies using Poetry by running the following command at the project root: +```bash +poetry install +``` +This will create a new virtual environment if one does not already exist and install the dependencies into it. + +### 3. Run the tests: + - To run all the tests, use the following command or use the Makefile: +```bash +poetry run pytest -v --cov-report term-missing --disable-warnings --cov=app tests/ +``` + +### 4. Run the transformer: + - To run the transformer, use the following command: +```bash +poetry run python run.py +``` diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..b231207 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,210 @@ +openapi: 3.0.3 +info: + title: SDX Transformer API + version: 1.0.0 + description: API for processing PCK and Prepop requests +paths: + /pck: + post: + summary: Process PCK request + description: "Convert a survey response into a PCK file" + operationId: processPck + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ListCollector' + - $ref: '#/components/schemas/Data' + parameters: + - name: survey_id + description: "The id of the survey" + example: "009" + in: query + required: true + schema: + type: string + - name: period_id + description: "The period of the collection exercise" + example: "202501" + in: query + required: true + schema: + type: string + - name: ru_ref + description: "The reference of the respondent" + example: "12345678901A" + in: query + required: true + schema: + type: string + - name: form_type + description: "The form type" + example: "0001" + in: query + required: true + schema: + type: string + - name: period_start_date + description: "The start date of the period in iso 8601 format (YYY-MM-DD)" + example: "2025-01-01" + in: query + required: true + schema: + type: string + - name: period_end_date + description: "The end date of the period in iso 8601 format (YYY-MM-DD)" + example: "2025-01-31" + in: query + required: true + schema: + type: string + - name: data_version + description: The version of schema of the data. + example: "0.0.3" + in: query + required: false + schema: + type: string + default: "0.0.1" + responses: + '200': + description: Successful response + content: + text/plain: + schema: + oneOf: + - type: string + - $ref: '#/components/schemas/SPP' + '400': + description: Bad request + '500': + description: Internal server error + /prepop: + post: + summary: Process Prepop request + description: "Convert prepopulated data into a supplementary data file" + operationId: processPrepop + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PrepopData' + parameters: + - name: survey_id + description: "The id of the survey" + example: "071" + in: query + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + '400': + description: Bad request + '500': + description: Internal server error +components: + schemas: + Data: + type: object + PrepopData: + type: object + additionalProperties: + type: array + items: + type: object + additionalProperties: + type: string + Answer: + type: object + properties: + answer_id: + type: string + value: + oneOf: + - type: object + - type: array + - type: string + list_item_id: + type: string + nullable: true + SupplementaryDataMapping: + type: object + properties: + identifier: + type: string + list_item_id: + type: string + Group: + type: object + properties: + items: + type: array + items: + type: string + name: + type: string + supplementary_data_mappings: + type: array + items: + $ref: '#/components/schemas/SupplementaryDataMapping' + nullable: true + AnswerCode: + type: object + properties: + answer_id: + type: string + code: + type: string + answer_value: + type: string + nullable: true + ListCollector: + type: object + properties: + answer_codes: + type: array + items: + $ref: '#/components/schemas/AnswerCode' + answers: + type: array + items: + $ref: '#/components/schemas/Answer' + lists: + type: array + items: + $ref: '#/components/schemas/Group' + PCK: + type: string + SPPResponse: + type: object + properties: + questioncode: + type: string + response: + type: string + instance: + type: integer + SPP: + type: object + properties: + formtype: + type: string + reference: + type: string + period: + type: string + survey: + type: string + responses: + type: array + items: + $ref: '#/components/schemas/SPPResponse' diff --git a/app/build_spec.py b/app/build_spec.py deleted file mode 100644 index 1f6422f..0000000 --- a/app/build_spec.py +++ /dev/null @@ -1,69 +0,0 @@ -import json -from collections.abc import Mapping -from os.path import exists -from typing import TypeVar, Literal - -import yaml -from sdx_gcp.app import get_logger -from sdx_gcp.errors import DataError - -from app.definitions import BuildSpec, BuildSpecError, ParseTree -from app.formatters.formatter import Formatter -from app.transform.interpolate import interpolate -from app.transform.populate import resolve_value_fields - -logger = get_logger() - - -def get_build_spec(survey_id: str, survey_mapping: dict[str, str], subdir: str = "pck") -> BuildSpec: - """ - Looks up the relevant build spec for the submission provided. - """ - survey_name = survey_mapping.get(survey_id) - if survey_name is None: - raise DataError(f"Could not lookup survey id {survey_id}") - - filepath = f"build_specs/{subdir}/{survey_name}.yaml" - if exists(filepath): - logger.info(f"Getting build spec from {filepath}") - with open(filepath) as y: - build_spec: BuildSpec = yaml.safe_load(y.read()) - - else: - filepath = f"build_specs/{subdir}/{survey_name}.json" - logger.info(f"Getting build spec from {filepath}") - with open(filepath) as j: - build_spec: BuildSpec = json.load(j) - - return build_spec - - -template_type = Literal["template", "looped"] - - -def interpolate_build_spec(build_spec: BuildSpec, template: template_type = "template") -> ParseTree: - if template == "looped": - if template not in build_spec: - template: template_type = "template" - - if 'transforms' in build_spec: - parse_tree: ParseTree = interpolate(build_spec[template], build_spec["transforms"]) - else: - parse_tree: ParseTree = build_spec[template] - return resolve_value_fields(parse_tree) - - -T = TypeVar("T", bound=Formatter) - - -def get_formatter(build_spec: BuildSpec, formatter_mapping: Mapping[str, T.__class__]) -> T: - f: T.__class__ = formatter_mapping.get(build_spec["target"]) - if f is None: - raise BuildSpecError(f"Unable to find formatter for target: {build_spec['target']}") - - period_format = build_spec["period_format"] - pck_period_format = build_spec["pck_period_format"] if "pck_period_format" in build_spec else period_format - form_mapping = build_spec["form_mapping"] if "form_mapping" in build_spec else {} - - formatter: T = f(build_spec["period_format"], pck_period_format, form_mapping) - return formatter diff --git a/app/config/dependencies.py b/app/config/dependencies.py new file mode 100644 index 0000000..1a1bb07 --- /dev/null +++ b/app/config/dependencies.py @@ -0,0 +1,81 @@ +from collections.abc import Callable + +from app.config.formatters import _formatter_mapping +from app.config.specs import _build_spec_mapping, _prepop_spec_mapping +from app.config.functions import _function_lookup +from app.definitions.input import SurveyMetadata +from app.definitions.executor import ExecutorBase +from app.definitions.mapper import BuildSpecMappingBase, PrepopMappingBase, FormatterMappingBase +from app.definitions.repository import BuildSpecRepositoryBase +from app.services.mappers.formatter_mappings import FormatterMapping +from app.services.mappers.spec_mappings import BuildSpecMapping, PrepopSpecMapping +from app.repositories.file_repository import BuildSpecFileRepository +from app.services.transform.execute import Executor +from app.transformers.flat import FlatSpecTransformer +from app.transformers.looped import LoopedSpecTransformer +from app.transformers.prepop import PrepopTransformer + + +def get_spec_repository() -> BuildSpecRepositoryBase: + return BuildSpecFileRepository() + + +def get_build_spec_mapping(repository: BuildSpecRepositoryBase) -> BuildSpecMappingBase: + return BuildSpecMapping(_build_spec_mapping, repository) + + +def get_prepop_spec_mapping(repository: BuildSpecRepositoryBase) -> PrepopMappingBase: + return PrepopSpecMapping(_prepop_spec_mapping, repository) + + +def get_formatter_mapping() -> FormatterMappingBase: + return FormatterMapping(_formatter_mapping) + + +def get_func_lookup() -> dict[str, Callable]: + return _function_lookup + + +def get_executor(func_lookup: dict[str, Callable]) -> ExecutorBase: + return Executor(func_lookup) + + +def get_flat_transformer( + survey_metadata: SurveyMetadata, + spec_mapping: BuildSpecMappingBase, + executor: ExecutorBase, + formatter_mapping: FormatterMappingBase +) -> FlatSpecTransformer: + + return FlatSpecTransformer( + survey_metadata, + spec_mapping, + executor, + formatter_mapping) + + +def get_looped_transformer( + survey_metadata: SurveyMetadata, + spec_mapping: BuildSpecMappingBase, + executor: ExecutorBase, + formatter_mapping: FormatterMappingBase +) -> LoopedSpecTransformer: + + return LoopedSpecTransformer( + survey_metadata, + spec_mapping, + executor, + formatter_mapping) + + +def get_prepop_transformer( + survey_id: str, + spec_mapping: PrepopMappingBase, + executor: ExecutorBase, + formatter_mapping: FormatterMappingBase +) -> PrepopTransformer: + return PrepopTransformer( + survey_id, + spec_mapping, + executor, + formatter_mapping) diff --git a/app/config/formatters.py b/app/config/formatters.py new file mode 100644 index 0000000..cd9e64d --- /dev/null +++ b/app/config/formatters.py @@ -0,0 +1,21 @@ +from app.services.mappers.formatter_selectors import FormatterSelector +from app.services.formatters.cora_formatter import CORAFormatter, MESFormatter +from app.services.formatters.cora_looping_formatter import CORALoopingFormatter +from app.services.formatters.cs_formatter import CSFormatter +from app.services.formatters.cs_looping_formatter import CSLoopingFormatter +from app.services.formatters.idbr_looping_formatter import IDBRLoopingFormatter +from app.services.formatters.json_formatter import JSONFormatter +from app.services.formatters.open_road_formatter import OpenRoadFormatter +from app.services.formatters.spp_formatter import SPPFormatter +from app.services.formatters.spp_looping_formatter import SPPLoopingFormatter + + +_formatter_mapping = { + "CORA": FormatterSelector(CORAFormatter, CORALoopingFormatter), + "CORA_MES": FormatterSelector(MESFormatter), + "CS": FormatterSelector(CSFormatter, CSLoopingFormatter), + "OpenROAD": FormatterSelector(OpenRoadFormatter), + "SPP": FormatterSelector(SPPFormatter, SPPLoopingFormatter), + "IDBR": FormatterSelector(IDBRLoopingFormatter), + "JSON": FormatterSelector(JSONFormatter), +} diff --git a/app/config/functions.py b/app/config/functions.py new file mode 100644 index 0000000..4062941 --- /dev/null +++ b/app/config/functions.py @@ -0,0 +1,46 @@ +from collections.abc import Callable + +from app.services.transform.functions.compound import currency_thousands, period_start, period_end +from app.services.transform.functions.general import no_transform, exists, any_exists, lookup +from app.services.transform.functions.lists import as_list, append_to_list, prepend_to_list, trim_list +from app.services.transform.functions.numerical import round_half_up, aggregate, mean, number_equals, total, divide +from app.services.transform.functions.string import (starts_with, contains, any_contains, + concat, carve, string_padding, + space_split, postcode_start, postcode_end) +from app.services.transform.functions.time import (to_date, any_date, start_of_month, + end_of_month, start_of_year, end_of_year) + +_function_lookup: dict[str, Callable] = { + "VALUE": no_transform, + "EXISTS": exists, + "ANY_EXISTS": any_exists, + "LOOKUP": lookup, + "STARTS_WITH": starts_with, + "CONTAINS": contains, + "ANY_CONTAINS": any_contains, + "CONCAT": concat, + "TO_DATE": to_date, + "ANY_DATE": any_date, + "START_OF_MONTH": start_of_month, + "END_OF_MONTH": end_of_month, + "START_OF_YEAR": start_of_year, + "END_OF_YEAR": end_of_year, + "ROUND": round_half_up, + "TOTAL": total, + "DIVIDE": divide, + "AGGREGATE": aggregate, + "MEAN": mean, + "NUMBER_EQUALS": number_equals, + "CURRENCY_THOUSANDS": currency_thousands, + "PERIOD_START": period_start, + "PERIOD_END": period_end, + "CARVE": carve, + "AS_LIST": as_list, + "APPEND_TO_LIST": append_to_list, + "PREPEND_TO_LIST": prepend_to_list, + "TRIM_LIST": trim_list, + "PADDING": string_padding, + "SPACE_SPLIT": space_split, + "POSTCODE_START": postcode_start, + "POSTCODE_END": postcode_end, +} diff --git a/app/config/specs.py b/app/config/specs.py new file mode 100644 index 0000000..273c556 --- /dev/null +++ b/app/config/specs.py @@ -0,0 +1,48 @@ +from app.definitions.input import SurveyMetadata +from app.definitions.mapper import Selector +from app.services.mappers.spec_selectors import BuildSpecSelector, BuildSpecPeriodSelector, PrepopSelector + + +_build_spec_mapping: dict[str, Selector[SurveyMetadata, str]] = { + "001": BuildSpecSelector("looping"), + "002": BuildSpecSelector("berd"), + "009": BuildSpecPeriodSelector(period_id="2503", before="mbs", after_or_equal="mbs-spp"), + "017": BuildSpecSelector("stocks"), + "019": BuildSpecSelector("qcas"), + "024": BuildSpecSelector("fuels"), + "066": BuildSpecSelector("qsl"), + "068": BuildSpecSelector("qrt"), + "071": BuildSpecSelector("qs"), + "073": BuildSpecSelector("blocks"), + "074": BuildSpecSelector("bricks"), + "076": BuildSpecSelector("qsm"), + "092": BuildSpecSelector("mes"), + "127": BuildSpecSelector("mcg"), + "134": BuildSpecSelector("mwss"), + "139": BuildSpecPeriodSelector(period_id="2503", before="qbs", after_or_equal="qbs-spp"), + "144": BuildSpecSelector("ukis"), + "160": BuildSpecSelector("qpses"), + "165": BuildSpecSelector("qpsespb"), + "169": BuildSpecSelector("qpsesrap"), + "171": BuildSpecSelector("acas"), + "182": BuildSpecSelector("vacancies"), + "183": BuildSpecSelector("vacancies"), + "184": BuildSpecSelector("vacancies"), + "185": BuildSpecSelector("vacancies"), + "187": BuildSpecSelector("des"), + "194": BuildSpecSelector("rails"), + "202": BuildSpecSelector("abs"), + "221": BuildSpecSelector("bres"), + "228": BuildSpecPeriodSelector(period_id="2503", before="construction", after_or_equal="construction-spp"), + "999": BuildSpecSelector("looping-spp"), +} + + +_prepop_spec_mapping: dict[str, Selector[str, str]] = { + "066": PrepopSelector("land-prepop"), + "068": PrepopSelector("tiles-prepop"), + "071": PrepopSelector("slate-prepop"), + "076": PrepopSelector("marine-prepop"), + "221": PrepopSelector("bres-prepop"), + "241": PrepopSelector("brs-prepop"), +} diff --git a/app/controllers/flat.py b/app/controllers/flat.py new file mode 100644 index 0000000..a618967 --- /dev/null +++ b/app/controllers/flat.py @@ -0,0 +1,38 @@ + +from sdx_gcp.app import get_logger + +from app.config.dependencies import get_flat_transformer, get_build_spec_mapping, get_executor, get_func_lookup, \ + get_spec_repository, get_formatter_mapping +from app.definitions.spec import ParseTree +from app.definitions.input import Data, SurveyMetadata, Value +from app.definitions.output import PCK +from app.definitions.transformer import TransformerBase + + +logger = get_logger() + + +def get_pck(submission_data: Data, survey_metadata: SurveyMetadata) -> PCK: + """ + Performs the steps required to generate a pck file from the submission data. + """ + transformer: TransformerBase = get_flat_transformer( + survey_metadata, + get_build_spec_mapping(get_spec_repository()), + get_executor(get_func_lookup()), + get_formatter_mapping(), + ) + + add_metadata_to_input_data(submission_data, survey_metadata) + tree: ParseTree = transformer.interpolate() + transformed_data: dict[str, Value] = transformer.run(tree, submission_data) + logger.info("Completed data transformation") + formatter = transformer.get_formatter() + pck = formatter.generate_pck(transformed_data, survey_metadata) + logger.info("Generated pck file") + return pck + + +def add_metadata_to_input_data(submission_data: Data, survey_metadata: SurveyMetadata): + for k, v in survey_metadata.items(): + submission_data[k] = v diff --git a/app/pck_managers/looped.py b/app/controllers/looped.py similarity index 67% rename from app/pck_managers/looped.py rename to app/controllers/looped.py index 480aea1..bb03946 100644 --- a/app/pck_managers/looped.py +++ b/app/controllers/looped.py @@ -1,41 +1,19 @@ from sdx_gcp.app import get_logger from sdx_gcp.errors import DataError -from app.berd.berd_transformer import berd_to_spp, berd_to_image -from app.build_spec import get_build_spec, get_formatter, interpolate_build_spec -from app.definitions import BuildSpec, ParseTree, SurveyMetadata, \ - ListCollector, LoopedData, Data, AnswerCode, Value, PCK, Empty -from app.formatters.cora_looping_formatter import CORALoopingFormatter -from app.formatters.cs_looping_formatter import CSLoopingFormatter -from app.formatters.idbr_looping_formatter import IDBRLoopingFormatter -from app.formatters.image_looping_formatter import ImageLoopingFormatter -from app.formatters.looping_formatter import LoopingFormatter -from app.formatters.spp_looping_formatter import SPPLoopingFormatter -from app.transform.execute import execute -from app.transform.populate import populate_mappings +from app.services.berd.berd_transformer import berd_to_spp +from app.config.dependencies import get_looped_transformer, get_build_spec_mapping, get_spec_repository, get_executor, \ + get_func_lookup, get_formatter_mapping +from app.definitions.input import Data, SurveyMetadata, AnswerCode, ListCollector, LoopedData, Empty, Value +from app.definitions.output import PCK +from app.definitions.spec import ParseTree +from app.services.formatters.looping_formatter import LoopingFormatter +from app.transformers.looped import LoopedSpecTransformer logger = get_logger() -survey_mapping: dict[str, str] = { - "001": "looping", - "066": "qsl", - "068": "qrt", - "071": "qs", - "076": "qsm", - "221": "bres", - "999": "looping-spp", -} - -formatter_mapping: dict[str, LoopingFormatter.__class__] = { - "CORA": CORALoopingFormatter, - "SPP": SPPLoopingFormatter, - "Image": ImageLoopingFormatter, - "CS": CSLoopingFormatter, - "IDBR": IDBRLoopingFormatter, -} - - -def get_looping(list_data: ListCollector, survey_metadata: SurveyMetadata, use_image_formatter: bool = False) -> PCK: + +def get_looping(list_data: ListCollector, survey_metadata: SurveyMetadata) -> PCK: """ Performs the steps required to transform looped data. """ @@ -43,18 +21,20 @@ def get_looping(list_data: ListCollector, survey_metadata: SurveyMetadata, use_i # temporary solution for Berd -------------- if survey_metadata["survey_id"] == "002" and survey_metadata["form_type"] == "0001": - if use_image_formatter: - return berd_to_image(list_data, survey_metadata) - else: - return berd_to_spp(list_data, survey_metadata) + return berd_to_spp(list_data, survey_metadata) # ------------------------------------------ - build_spec: BuildSpec = get_build_spec(survey_metadata["survey_id"], survey_mapping, "looping") + transformer: LoopedSpecTransformer = get_looped_transformer( + survey_metadata, + get_build_spec_mapping(get_spec_repository()), + get_executor(get_func_lookup()), + get_formatter_mapping(), + ) looped_data: LoopedData = convert_to_looped_data(list_data) data_section: Data = looped_data['data_section'] # CS can only handle one instance. Therefore, convert all looped data back into 'regular' data - if build_spec["target"] == "CS": + if transformer.get_spec()["target"] == "CS": looped_sections: dict[str, dict[str, Data]] = looped_data["looped_sections"] item_dict: dict[str, Data] for item_dict in looped_sections.values(): @@ -64,27 +44,21 @@ def get_looping(list_data: ListCollector, survey_metadata: SurveyMetadata, use_i looped_data["looped_sections"] = {} - # for images use a fake build spec that maps all answers without transform - if use_image_formatter: - build_spec: BuildSpec = get_image_spec(list_data, survey_metadata["survey_id"]) - - full_tree: ParseTree = interpolate_build_spec(build_spec) - populated_tree: ParseTree = populate_mappings(full_tree, data_section) - transformed_data_section: dict[str, Value] = execute(populated_tree) + full_tree: ParseTree = transformer.interpolate() + transformed_data_section: dict[str, Value] = transformer.run(full_tree, data_section) result_data = {k: v for k, v in transformed_data_section.items() if v is not Empty} - formatter: LoopingFormatter = get_formatter(build_spec, formatter_mapping) + formatter: LoopingFormatter = transformer.get_formatter() formatter.set_original(list_data) looped_sections: dict[str, dict[str, Data]] = looped_data['looped_sections'] if looped_sections: - looped_tree: ParseTree = interpolate_build_spec(build_spec, "looped") + looped_tree: ParseTree = transformer.interpolate_looped() for data_dict in looped_sections.values(): instance_id = 1 for list_item_id, d in data_dict.items(): - populated_tree: ParseTree = populate_mappings(looped_tree, d) - transformed_data: dict[str, Value] = execute(populated_tree) + transformed_data: dict[str, Value] = transformer.run(looped_tree, d) # remove any values that are empty or already appear in the data section result = {k: v for k, v in transformed_data.items() if v is not Empty} formatter.create_or_update_instance(instance_id=str(instance_id), data=result, @@ -207,25 +181,3 @@ def convert_to_looped_data(data: ListCollector) -> LoopedData: "looped_sections": looped_sections, "data_section": data_section } - - -def get_image_spec(list_data: ListCollector, survey_id: str) -> BuildSpec: - """ - Create a build spec for image files. - This involves mapping all the qcodes that - appear in the data without any transformation - """ - template: dict[str, str] = {} - a: AnswerCode - for a in list_data["answer_codes"]: - qcode: str = a["code"] - template[qcode] = f'#{qcode}' - - return { - "title": "Generic Image Build Spec", - "survey_id": survey_id, - "target": "Image", - "period_format": "YYYYMM", - "template": template, - "transforms": {} - } diff --git a/app/pck_managers/prepop.py b/app/controllers/prepop.py similarity index 71% rename from app/pck_managers/prepop.py rename to app/controllers/prepop.py index 6f828bc..c1aa66c 100644 --- a/app/pck_managers/prepop.py +++ b/app/controllers/prepop.py @@ -4,30 +4,29 @@ from sdx_gcp.app import get_logger from sdx_gcp.errors import DataError -from app.build_spec import get_build_spec, interpolate_build_spec -from app.definitions import BuildSpec, ParseTree, PrepopData, Template, Identifier, Field -from app.transform.clean import clean -from app.transform.execute import execute -from app.transform.populate import populate_mappings +from app.config.dependencies import get_prepop_transformer, get_prepop_spec_mapping, get_spec_repository, get_executor, \ + get_func_lookup, get_formatter_mapping +from app.definitions.spec import BuildSpec, ParseTree, Template +from app.definitions.input import Identifier, PrepopData, Field +from app.definitions.transformer import TransformerBase +from app.services.transform.clean import clean logger = get_logger() -survey_mapping: dict[str, str] = { - "066": "land", - "068": "tiles", - "071": "slate", - "076": "marine", - "221": "bres", - "241": "brs", -} - def get_prepop(prepop_data: PrepopData, survey_id: str) -> dict[Identifier: Template]: """ Performs the steps required to transform prepopulated data. """ - build_spec: BuildSpec = get_build_spec(survey_id, survey_mapping, "prepop") - parse_tree: ParseTree = interpolate_build_spec(build_spec) + transformer: TransformerBase = get_prepop_transformer( + survey_id, + get_prepop_spec_mapping(get_spec_repository()), + get_executor(get_func_lookup()), + get_formatter_mapping(), + ) + + build_spec: BuildSpec = transformer.get_spec() + parse_tree: ParseTree = transformer.interpolate() result: dict[Identifier: Template] = {} @@ -37,8 +36,7 @@ def get_prepop(prepop_data: PrepopData, survey_id: str) -> dict[Identifier: Temp for ru_ref, data_list in prepop_data.items(): items: list[Template] = [] for data in data_list: - populated_tree: ParseTree = populate_mappings(parse_tree, data) - result_item: Template = execute(populated_tree) + result_item: Template = transformer.run(parse_tree, data) result_item = clean(result_item) items.append(result_item) diff --git a/app/definitions.py b/app/definitions.py deleted file mode 100644 index d70e6c8..0000000 --- a/app/definitions.py +++ /dev/null @@ -1,116 +0,0 @@ -from typing import TypedDict, NotRequired - - -Data = dict[str, str] - -Field = dict | list | str | None - -Template = dict[str, Field] - - -class Transform(TypedDict): - name: str - args: dict[str, Field] - post: NotRequired[str] - - -Transforms = dict[str, Transform] - -Empty = None - -Value = str | Empty - -ParseTree = dict[str, Field] - - -class BuildSpec(TypedDict): - title: str - survey_id: str - period_format: str - pck_period_format: NotRequired[str] - form_mapping: NotRequired[dict[str, str]] - target: str - item_list_path: NotRequired[str] - template: Template - looped: NotRequired[Template] - transforms: Transforms - - -iso_8601_date = str # YYYY-MM-DD - - -class SurveyMetadata(TypedDict): - survey_id: str - period_id: str - ru_ref: str - form_type: str - period_start_date: iso_8601_date - period_end_date: iso_8601_date - - -Identifier = str - -PrepopData = dict[Identifier, list[Data]] - - -PCK = str - - -class BuildSpecError(Exception): - pass - - -class Answer(TypedDict): - answer_id: str - value: Field - list_item_id: NotRequired[str] - - -class SupplementaryDataMapping(TypedDict): - identifier: str - list_item_id: str - - -class Group(TypedDict): - items: list[str] - name: str - supplementary_data_mappings: NotRequired[list[SupplementaryDataMapping]] - - -class AnswerCode(TypedDict): - answer_id: str - code: str - answer_value: NotRequired[str] - - -class ListCollector(TypedDict): - answer_codes: list[AnswerCode] - answers: list[Answer] - lists: list[Group] - - -# Our top level looping object -class LoopedData(TypedDict): - looped_sections: dict[str, dict[str, Data]] - data_section: dict[str, Value] - - -class SPPResponse(TypedDict): - questioncode: str - response: str - instance: int - - -class SPP(TypedDict): - formtype: str - reference: str - period: str - survey: str - responses: list[SPPResponse] - - -class ImageResponse(TypedDict): - questioncode: str - response: str - instance: int - sd_identifier: NotRequired[str] diff --git a/app/definitions/executor.py b/app/definitions/executor.py new file mode 100644 index 0000000..04cfb49 --- /dev/null +++ b/app/definitions/executor.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod + +from app.definitions.input import Value, Data +from app.definitions.spec import ParseTree, Template, Transforms + + +class ExecutorBase(ABC): + """ + Base class for all Executors. + An Executor runs the main transformation steps. + """ + + @abstractmethod + def interpolate(self, template: Template, transforms: Transforms) -> ParseTree: + pass + + @abstractmethod + def populate(self, tree: ParseTree, data: Data) -> ParseTree: + pass + + @abstractmethod + def execute(self, tree: ParseTree) -> dict[str, Value]: + pass diff --git a/app/definitions/formatter.py b/app/definitions/formatter.py new file mode 100644 index 0000000..60169f8 --- /dev/null +++ b/app/definitions/formatter.py @@ -0,0 +1,17 @@ +""" +Definition of the Formatter Base. +A formatter is a responsible for generating the final output structure +after the transformations have taken place. +""" + +from abc import ABC, abstractmethod + +from app.definitions.input import Value, SurveyMetadata +from app.definitions.output import PCK + + +class FormatterBase(ABC): + + @abstractmethod + def generate_pck(self, data: dict[str, Value], metadata: SurveyMetadata) -> PCK: + pass diff --git a/app/definitions/input.py b/app/definitions/input.py new file mode 100644 index 0000000..6e22466 --- /dev/null +++ b/app/definitions/input.py @@ -0,0 +1,59 @@ +""" +Definitions to describe the structure of the input data. +""" + +from typing import TypedDict, NotRequired, TypeAlias + +Empty: TypeAlias = None +Value = str | Empty +Field = dict | list | str | Empty +Data = dict[str, str] +iso_8601_date = str # YYYY-MM-DD + + +class SurveyMetadata(TypedDict): + survey_id: str + period_id: str + ru_ref: str + form_type: str + period_start_date: iso_8601_date + period_end_date: iso_8601_date + + +Identifier = str +PrepopData = dict[Identifier, list[Data]] + + +class Answer(TypedDict): + answer_id: str + value: Field + list_item_id: NotRequired[str] + + +class SupplementaryDataMapping(TypedDict): + identifier: str + list_item_id: str + + +class Group(TypedDict): + items: list[str] + name: str + supplementary_data_mappings: NotRequired[list[SupplementaryDataMapping]] + + +class AnswerCode(TypedDict): + answer_id: str + code: str + answer_value: NotRequired[str] + + +class ListCollector(TypedDict): + answer_codes: list[AnswerCode] + answers: list[Answer] + lists: list[Group] + + +# The top level looping object +class LoopedData(TypedDict): + looped_sections: dict[str, dict[str, Data]] + data_section: dict[str, Value] diff --git a/app/definitions/mapper.py b/app/definitions/mapper.py new file mode 100644 index 0000000..f12db8d --- /dev/null +++ b/app/definitions/mapper.py @@ -0,0 +1,56 @@ +""" +Definitions of the base Mapping classes. + +The mapping base classes are built from the generic Mapper class - a data structure +allowing for dynamic mappings based on the implementation of the Selector interface. +""" + +from abc import ABC, abstractmethod + +from app.definitions.input import SurveyMetadata +from app.definitions.formatter import FormatterBase +from app.definitions.spec import BuildSpec + + +class Selector[T, U](ABC): + + @abstractmethod + def choose(self, discriminator: T) -> U: + pass + + +class Mapper[T, U]: + + def __init__(self, mappings: dict[str, Selector[T, U]]): + self._mappings = mappings + + def get(self, key: str, discriminator: T) -> U: + return self._mappings.get(key).choose(discriminator) + + +class SpecMappingBase[S](Mapper[S, str], ABC): + + @abstractmethod + def get_build_spec(self, s: S) -> BuildSpec: + pass + + +class BuildSpecMappingBase(SpecMappingBase[SurveyMetadata], ABC): + + @abstractmethod + def get_build_spec(self, survey_metadata: SurveyMetadata) -> BuildSpec: + pass + + +class PrepopMappingBase(SpecMappingBase[str], ABC): + + @abstractmethod + def get_build_spec(self, survey_id: str) -> BuildSpec: + pass + + +class FormatterMappingBase[F: FormatterBase](Mapper[bool, F.__class__], ABC): + + @abstractmethod + def get_formatter(self, target: str, looped: bool = False) -> F.__class__: + pass diff --git a/app/definitions/output.py b/app/definitions/output.py new file mode 100644 index 0000000..61f3ba6 --- /dev/null +++ b/app/definitions/output.py @@ -0,0 +1,22 @@ +""" +Definitions to describe the structure of the output data. +""" + +from typing import TypedDict + + +PCK = str + + +class SPPResponse(TypedDict): + questioncode: str + response: str + instance: int + + +class SPP(TypedDict): + formtype: str + reference: str + period: str + survey: str + responses: list[SPPResponse] diff --git a/app/definitions/repository.py b/app/definitions/repository.py new file mode 100644 index 0000000..dbc8154 --- /dev/null +++ b/app/definitions/repository.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + +from app.definitions.spec import BuildSpec + + +class BuildSpecRepositoryBase(ABC): + """ + Base class for all repositories of build specs. + """ + + @abstractmethod + def get_build_spec(self, spec_name: str) -> BuildSpec: + pass diff --git a/app/definitions/spec.py b/app/definitions/spec.py new file mode 100644 index 0000000..3f55f2a --- /dev/null +++ b/app/definitions/spec.py @@ -0,0 +1,38 @@ +""" +Definitions describing the structure of a 'build spec'. +""" + +from typing import TypedDict, NotRequired + +from app.definitions.input import Field + + +Template = dict[str, Field] + +ParseTree = dict[str, Field] + + +class Transform(TypedDict): + name: str + args: dict[str, Field] + post: NotRequired[str] + + +Transforms = dict[str, Transform] + + +class BuildSpec(TypedDict): + title: str + survey_id: str + period_format: str + pck_period_format: NotRequired[str] + form_mapping: NotRequired[dict[str, str]] + target: str + item_list_path: NotRequired[str] + template: Template + looped: NotRequired[Template] + transforms: Transforms + + +class BuildSpecError(Exception): + pass diff --git a/app/definitions/transformer.py b/app/definitions/transformer.py new file mode 100644 index 0000000..8d234fb --- /dev/null +++ b/app/definitions/transformer.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + +from app.definitions.input import Data, Value +from app.definitions.formatter import FormatterBase +from app.definitions.spec import ParseTree, BuildSpec + + +class TransformerBase[F: FormatterBase](ABC): + """ + Base class for all Transformers. + A Transformer is responsible for all interactions with the build spec and the data. + """ + + @abstractmethod + def get_spec(self) -> BuildSpec: + pass + + @abstractmethod + def interpolate(self) -> ParseTree: + pass + + @abstractmethod + def run(self, tree: ParseTree, data: Data) -> dict[str, Value]: + pass + + @abstractmethod + def get_formatter(self) -> F: + pass diff --git a/app/formatters/formatter.py b/app/formatters/formatter.py deleted file mode 100644 index f8ced95..0000000 --- a/app/formatters/formatter.py +++ /dev/null @@ -1,70 +0,0 @@ -from app.definitions import Value, SurveyMetadata, PCK, BuildSpecError - - -class Formatter: - - def __init__(self, period_format: str, pck_period_format: str, form_mapping: dict[str, str] = {}): - self._period_format = period_format - self._pck_period_format = pck_period_format - self._form_mapping = form_mapping - - def get_form_type(self, form_type: str) -> str: - if form_type in self._form_mapping: - return self._form_mapping[form_type] - else: - return form_type - - def generate_pck(self, data: dict[str, Value], metadata: SurveyMetadata) -> PCK: - """Write a PCK file.""" - pck_lines = self._pck_lines(data, metadata) - output = "\n".join(pck_lines) - return output + "\n" - - def _pck_lines(self, data: dict[str, Value], metadata: SurveyMetadata) -> list[str]: - pass - - def convert_period(self, period: str) -> str: - - period_format = self._period_format - - # If the period does not match the period format, - # use a default input format, based on the length of the period - if len(period) != len(period_format): - if len(period) == 6: - period_format = "YYYYMM" - elif len(period) == 4: - period_format = "YYMM" - elif len(period) == 2: - period_format = "YY" - - symbols: dict[str, list[str]] = { - "D": [], - "M": [], - "Y": [] - } - - for i in range(0, len(period)): - k = period_format[i] - if k not in symbols: - raise BuildSpecError(f"Build spec period in wrong format {period_format}") - symbols[k].append(period[i]) - - if self._pck_period_format.count("Y") == 4: - if len(symbols["Y"]) == 2: - symbols["Y"].insert(0, "0") - symbols["Y"].insert(0, "2") - elif self._pck_period_format.count("Y") == 2: - if len(symbols["Y"]) == 4: - symbols["Y"].pop(0) - symbols["Y"].pop(0) - - if self._pck_period_format.count("M") == 2: - if len(symbols["M"]) == 0: - symbols["M"].append("0") - symbols["M"].append("1") - - result = "" - for f in self._pck_period_format: - result += symbols[f].pop(0) - - return result diff --git a/app/formatters/image_looping_formatter.py b/app/formatters/image_looping_formatter.py deleted file mode 100644 index 8e5f258..0000000 --- a/app/formatters/image_looping_formatter.py +++ /dev/null @@ -1,42 +0,0 @@ -import json - -from app.definitions import Value, SurveyMetadata, PCK, ImageResponse, SupplementaryDataMapping -from app.formatters.looping_formatter import LoopingFormatter - - -class ImageLoopingFormatter(LoopingFormatter): - - def generate_pck(self, data_section: dict[str, Value], metadata: SurveyMetadata) -> PCK: - - sd_identifiers: dict[str, str] = {} - for group in self.original_data["lists"]: - if "supplementary_data_mappings" in group: - mapping: SupplementaryDataMapping - for mapping in group["supplementary_data_mappings"]: - sd_identifiers[mapping["list_item_id"]] = mapping["identifier"] - - result: list[ImageResponse] = [] - - for qcode, value in data_section.items(): - result.append({ - "questioncode": qcode, - "response": value, - "instance": 0 - }) - - for instance_id, instance_data_list in self._instances.items(): - for instance_data in instance_data_list: - for qcode, value in instance_data["data"].items(): - response: ImageResponse = { - "questioncode": qcode, - "response": value, - "instance": int(instance_id) - } - - if instance_data["list_item_id"] in sd_identifiers: - identifier = sd_identifiers[instance_data["list_item_id"]] - response["sd_identifier"] = identifier - - result.append(response) - - return json.dumps(result) diff --git a/app/pck_managers/mapped.py b/app/pck_managers/mapped.py deleted file mode 100644 index bfbe9e8..0000000 --- a/app/pck_managers/mapped.py +++ /dev/null @@ -1,78 +0,0 @@ - -from sdx_gcp.app import get_logger - -from app.build_spec import get_build_spec, get_formatter, interpolate_build_spec -from app.definitions import BuildSpec, ParseTree, Value, PCK, Data, SurveyMetadata -from app.formatters.cora_formatter import CORAFormatter, MESFormatter -from app.formatters.cs_formatter import CSFormatter -from app.formatters.formatter import Formatter -from app.formatters.json_formatter import JSONFormatter -from app.formatters.open_road_formatter import OpenRoadFormatter -from app.formatters.spp_formatter import SPPFormatter -from app.transform.execute import execute -from app.transform.populate import populate_mappings - -logger = get_logger() - -survey_mapping: dict[str, str] = { - "002": "berd", - "009": "mbs", - "017": "stocks", - "019": "qcas", - "024": "fuels", - "073": "blocks", - "074": "bricks", - "092": "mes", - "127": "mcg", - "134": "mwss", - "139": "qbs", - "144": "ukis", - "160": "qpses", - "165": "qpsespb", - "169": "qpsesrap", - "171": "acas", - "182": "vacancies", - "183": "vacancies", - "184": "vacancies", - "185": "vacancies", - "187": "des", - "194": "rails", - "202": "abs", - "228": "construction", -} - - -formatter_mapping: dict[str, Formatter.__class__] = { - "CORA": CORAFormatter, - "CORA_MES": MESFormatter, - "CS": CSFormatter, - "OpenROAD": OpenRoadFormatter, - "SPP": SPPFormatter, - "JSON": JSONFormatter, -} - - -def get_pck(submission_data: Data, survey_metadata: SurveyMetadata) -> PCK: - """ - Performs the steps required to generate a pck file from the submission data. - """ - build_spec: BuildSpec = get_build_spec(survey_metadata["survey_id"], survey_mapping) - add_metadata_to_input_data(submission_data, survey_metadata) - transformed_data: dict[str, Value] = transform(submission_data, build_spec) - formatter = get_formatter(build_spec, formatter_mapping) - pck = formatter.generate_pck(transformed_data, survey_metadata) - logger.info("Generated pck file") - return pck - - -def add_metadata_to_input_data(submission_data: Data, survey_metadata: SurveyMetadata): - for k, v in survey_metadata.items(): - submission_data[k] = v - - -def transform(data: Data, build_spec: BuildSpec) -> dict[str, Value]: - full_tree: ParseTree = interpolate_build_spec(build_spec) - populated_tree: ParseTree = populate_mappings(full_tree, data) - result_data: dict[str, Value] = execute(populated_tree) - logger.info("Completed data transformation") - return result_data diff --git a/app/repositories/file_repository.py b/app/repositories/file_repository.py new file mode 100644 index 0000000..48b04ad --- /dev/null +++ b/app/repositories/file_repository.py @@ -0,0 +1,30 @@ +import json +from os.path import exists +import yaml +from sdx_gcp.app import get_logger +from app.definitions.spec import BuildSpec +from app.definitions.repository import BuildSpecRepositoryBase + +logger = get_logger() + + +class BuildSpecFileRepository(BuildSpecRepositoryBase): + dir: str = "build_specs" + + def get_build_spec(self, spec_name: str) -> BuildSpec: + """ + Looks up the relevant build spec for the submission provided. + """ + filepath = f"{self.dir}/{spec_name}.yaml" + if exists(filepath): + logger.info(f"Getting build spec from {filepath}") + with open(filepath) as y: + build_spec: BuildSpec = yaml.safe_load(y.read()) + + else: + filepath = f"{self.dir}/{spec_name}.json" + logger.info(f"Getting build spec from {filepath}") + with open(filepath) as j: + build_spec: BuildSpec = json.load(j) + + return build_spec diff --git a/app/routes.py b/app/routes.py index 74a4ced..ce7464b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,16 +2,20 @@ from sdx_gcp.app import get_logger, SdxApp from sdx_gcp.errors import DataError -from app.definitions import PrepopData, Identifier, Template, PCK, Data, SurveyMetadata, ListCollector -from app.pck_managers.looped import get_looping -from app.pck_managers.mapped import get_pck -from app.pck_managers.prepop import get_prepop +from app.definitions.spec import Template +from app.definitions.input import Data, SurveyMetadata, Identifier, PrepopData, ListCollector +from app.definitions.output import PCK +from app.controllers.looped import get_looping +from app.controllers.flat import get_pck +from app.controllers.prepop import get_prepop logger = get_logger() def process_pck(req: Request, _tx_id: TX_ID): + """Process a request to convert submission data to a PCK file.""" + logger.info("Received pck request") survey_metadata: SurveyMetadata = { @@ -25,13 +29,6 @@ def process_pck(req: Request, _tx_id: TX_ID): logger.info("Received Parameters", **survey_metadata) - use_image_formatter: bool = req.args.get("use_image_formatter", "False").upper() == "TRUE" - - if use_image_formatter: - logger.info("Using image transformer") - else: - logger.info("NOT using image transformer") - for k, v in survey_metadata.items(): if v == "": raise DataError(f"Missing required parameter {k} from request") @@ -43,7 +40,7 @@ def process_pck(req: Request, _tx_id: TX_ID): if submission_data is None: raise DataError("Submission data is not in json format") - pck: PCK = get_looping(submission_data, survey_metadata, use_image_formatter=use_image_formatter) + pck: PCK = get_looping(submission_data, survey_metadata) else: submission_data: Data = req.get_json(force=True, silent=True) @@ -57,6 +54,8 @@ def process_pck(req: Request, _tx_id: TX_ID): def process_prepop(req: Request, _tx_id: TX_ID): + """Process a request to convert pre-population data into supplementary format.""" + logger.info("Received prepop request") prepop_data: PrepopData = req.get_json(force=True, silent=True) survey_id: str = req.args.get("survey_id") diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/berd/__init__.py b/app/services/berd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/berd/berd_transformer.py b/app/services/berd/berd_transformer.py similarity index 76% rename from app/berd/berd_transformer.py rename to app/services/berd/berd_transformer.py index c633270..b48dc3e 100644 --- a/app/berd/berd_transformer.py +++ b/app/services/berd/berd_transformer.py @@ -3,11 +3,11 @@ from sdx_gcp.app import get_logger -from app.berd.collect_items import collect_list_items -from app.berd.convert_data import extract_answers, convert_to_spp, convert_civil_defence, remove_prepend_values -from app.berd.definitions import SPP -from app.definitions import ListCollector, SurveyMetadata, PCK -from app.definitions import SPP as SppResult +from app.services.berd.collect_items import collect_list_items +from app.services.berd.convert_data import extract_answers, convert_to_spp, convert_civil_defence, remove_prepend_values +from app.services.berd.definitions import SPP +from app.definitions.input import SurveyMetadata, ListCollector +from app.definitions.output import SPP as SppResult, PCK logger = get_logger() diff --git a/app/berd/collect_items.py b/app/services/berd/collect_items.py similarity index 95% rename from app/berd/collect_items.py rename to app/services/berd/collect_items.py index c83e716..39c948f 100644 --- a/app/berd/collect_items.py +++ b/app/services/berd/collect_items.py @@ -1,4 +1,4 @@ -from app.berd.definitions import Answer +from app.services.berd.definitions import Answer def collect_list_items(answer_list: list[Answer]) -> list[Answer]: diff --git a/app/berd/convert_data.py b/app/services/berd/convert_data.py similarity index 98% rename from app/berd/convert_data.py rename to app/services/berd/convert_data.py index 8e0e1da..c90d639 100644 --- a/app/berd/convert_data.py +++ b/app/services/berd/convert_data.py @@ -1,6 +1,6 @@ from sdx_gcp.app import get_logger -from app.berd.definitions import SPP, Answer +from app.services.berd.definitions import SPP, Answer logger = get_logger() diff --git a/app/berd/definitions.py b/app/services/berd/definitions.py similarity index 100% rename from app/berd/definitions.py rename to app/services/berd/definitions.py diff --git a/app/services/formatters/__init__.py b/app/services/formatters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/formatters/cora_formatter.py b/app/services/formatters/cora_formatter.py similarity index 90% rename from app/formatters/cora_formatter.py rename to app/services/formatters/cora_formatter.py index 0b93c4f..90405fa 100644 --- a/app/formatters/cora_formatter.py +++ b/app/services/formatters/cora_formatter.py @@ -1,5 +1,5 @@ -from app.definitions import Value, SurveyMetadata, Empty -from app.formatters.formatter import Formatter +from app.definitions.input import SurveyMetadata, Empty, Value +from app.services.formatters.formatter import Formatter class CORAFormatter(Formatter): diff --git a/app/formatters/cora_looping_formatter.py b/app/services/formatters/cora_looping_formatter.py similarity index 72% rename from app/formatters/cora_looping_formatter.py rename to app/services/formatters/cora_looping_formatter.py index fdee141..8cddad3 100644 --- a/app/formatters/cora_looping_formatter.py +++ b/app/services/formatters/cora_looping_formatter.py @@ -1,6 +1,7 @@ -from app.definitions import Value, SurveyMetadata, PCK -from app.formatters.cora_formatter import CORAFormatter -from app.formatters.looping_formatter import LoopingFormatter +from app.definitions.input import SurveyMetadata, Value +from app.definitions.output import PCK +from app.services.formatters.cora_formatter import CORAFormatter +from app.services.formatters.looping_formatter import LoopingFormatter class CORALoopingFormatter(CORAFormatter, LoopingFormatter): diff --git a/app/formatters/cs_formatter.py b/app/services/formatters/cs_formatter.py similarity index 93% rename from app/formatters/cs_formatter.py rename to app/services/formatters/cs_formatter.py index 9ef3406..6b2fb0e 100644 --- a/app/formatters/cs_formatter.py +++ b/app/services/formatters/cs_formatter.py @@ -1,5 +1,5 @@ -from app.definitions import SurveyMetadata, Value -from app.formatters.formatter import Formatter +from app.definitions.input import SurveyMetadata, Value +from app.services.formatters.formatter import Formatter class CSFormatter(Formatter): diff --git a/app/formatters/cs_looping_formatter.py b/app/services/formatters/cs_looping_formatter.py similarity index 72% rename from app/formatters/cs_looping_formatter.py rename to app/services/formatters/cs_looping_formatter.py index 2fe436e..b7b37a0 100644 --- a/app/formatters/cs_looping_formatter.py +++ b/app/services/formatters/cs_looping_formatter.py @@ -1,6 +1,7 @@ -from app.definitions import Value, SurveyMetadata, PCK -from app.formatters.cs_formatter import CSFormatter -from app.formatters.looping_formatter import LoopingFormatter +from app.definitions.input import SurveyMetadata, Value +from app.definitions.output import PCK +from app.services.formatters.cs_formatter import CSFormatter +from app.services.formatters.looping_formatter import LoopingFormatter class CSLoopingFormatter(CSFormatter, LoopingFormatter): diff --git a/app/services/formatters/formatter.py b/app/services/formatters/formatter.py new file mode 100644 index 0000000..934ad9e --- /dev/null +++ b/app/services/formatters/formatter.py @@ -0,0 +1,36 @@ +from app.definitions.formatter import FormatterBase +from app.definitions.spec import BuildSpecError +from app.definitions.input import SurveyMetadata, Value +from app.definitions.output import PCK +from app.services.period.period import PeriodFormatError, Period + + +class Formatter(FormatterBase): + + def __init__(self, period_format: str, pck_period_format: str, form_mapping: dict[str, str] = {}): + self._period_format = period_format + self._pck_period_format = pck_period_format + self._form_mapping = form_mapping + + def get_form_type(self, form_type: str) -> str: + if form_type in self._form_mapping: + return self._form_mapping[form_type] + else: + return form_type + + def generate_pck(self, data: dict[str, Value], metadata: SurveyMetadata) -> PCK: + """Write a PCK file.""" + pck_lines = self._pck_lines(data, metadata) + output = "\n".join(pck_lines) + return output + "\n" + + def _pck_lines(self, data: dict[str, Value], metadata: SurveyMetadata) -> list[str]: + pass + + def convert_period(self, period_id: str) -> str: + try: + period = Period(period_id, self._period_format) + return period.convert_to_format(self._pck_period_format) + + except PeriodFormatError as e: + raise BuildSpecError(f"Build spec period in wrong format {self._period_format}") from e diff --git a/app/formatters/idbr_formatter.py b/app/services/formatters/idbr_formatter.py similarity index 93% rename from app/formatters/idbr_formatter.py rename to app/services/formatters/idbr_formatter.py index ab568c9..0aa4690 100644 --- a/app/formatters/idbr_formatter.py +++ b/app/services/formatters/idbr_formatter.py @@ -1,7 +1,7 @@ from typing import Optional -from app.definitions import Value, SurveyMetadata, Empty -from app.formatters.formatter import Formatter +from app.definitions.input import SurveyMetadata, Empty, Value +from app.services.formatters.formatter import Formatter def _get_scan_number(metadata: SurveyMetadata, ref: Optional[str] = None) -> str: diff --git a/app/formatters/idbr_looping_formatter.py b/app/services/formatters/idbr_looping_formatter.py similarity index 87% rename from app/formatters/idbr_looping_formatter.py rename to app/services/formatters/idbr_looping_formatter.py index 9c595ff..0d5f88d 100644 --- a/app/formatters/idbr_looping_formatter.py +++ b/app/services/formatters/idbr_looping_formatter.py @@ -1,8 +1,9 @@ from typing import Final -from app.definitions import Value, SurveyMetadata, PCK, SupplementaryDataMapping -from app.formatters.idbr_formatter import IDBRFormatter -from app.formatters.looping_formatter import LoopingFormatter +from app.definitions.input import SurveyMetadata, SupplementaryDataMapping, Value +from app.definitions.output import PCK +from app.services.formatters.idbr_formatter import IDBRFormatter +from app.services.formatters.looping_formatter import LoopingFormatter DEFAULT_REF: Final[str] = "N0000000" LIST_ITEM_ID: Final[str] = "list_item_id" diff --git a/app/formatters/json_formatter.py b/app/services/formatters/json_formatter.py similarity index 72% rename from app/formatters/json_formatter.py rename to app/services/formatters/json_formatter.py index e1a2eeb..3b31b38 100644 --- a/app/formatters/json_formatter.py +++ b/app/services/formatters/json_formatter.py @@ -1,7 +1,8 @@ import json -from app.definitions import Value, SurveyMetadata, PCK, Empty -from app.formatters.formatter import Formatter +from app.definitions.input import SurveyMetadata, Empty, Value +from app.definitions.output import PCK +from app.services.formatters.formatter import Formatter class JSONFormatter(Formatter): diff --git a/app/formatters/looping_formatter.py b/app/services/formatters/looping_formatter.py similarity index 88% rename from app/formatters/looping_formatter.py rename to app/services/formatters/looping_formatter.py index dc496d2..36b93b5 100644 --- a/app/formatters/looping_formatter.py +++ b/app/services/formatters/looping_formatter.py @@ -1,7 +1,7 @@ from typing import TypedDict -from app.definitions import Value, ListCollector -from app.formatters.formatter import Formatter +from app.definitions.input import ListCollector, Value +from app.services.formatters.formatter import Formatter class InstanceData(TypedDict): diff --git a/app/formatters/open_road_formatter.py b/app/services/formatters/open_road_formatter.py similarity index 84% rename from app/formatters/open_road_formatter.py rename to app/services/formatters/open_road_formatter.py index 6546916..84f4424 100644 --- a/app/formatters/open_road_formatter.py +++ b/app/services/formatters/open_road_formatter.py @@ -1,7 +1,7 @@ from sdx_gcp.app import get_logger -from app.definitions import Value, SurveyMetadata, Empty -from app.formatters.formatter import Formatter +from app.definitions.input import SurveyMetadata, Empty, Value +from app.services.formatters.formatter import Formatter logger = get_logger() diff --git a/app/formatters/spp_formatter.py b/app/services/formatters/spp_formatter.py similarity index 84% rename from app/formatters/spp_formatter.py rename to app/services/formatters/spp_formatter.py index ff54101..57831f5 100644 --- a/app/formatters/spp_formatter.py +++ b/app/services/formatters/spp_formatter.py @@ -1,7 +1,8 @@ import json -from app.definitions import Value, SurveyMetadata, PCK, SPP, Empty -from app.formatters.formatter import Formatter +from app.definitions.input import SurveyMetadata, Empty, Value +from app.definitions.output import SPP, PCK +from app.services.formatters.formatter import Formatter class SPPFormatter(Formatter): diff --git a/app/formatters/spp_looping_formatter.py b/app/services/formatters/spp_looping_formatter.py similarity index 74% rename from app/formatters/spp_looping_formatter.py rename to app/services/formatters/spp_looping_formatter.py index 04c655f..ac2e363 100644 --- a/app/formatters/spp_looping_formatter.py +++ b/app/services/formatters/spp_looping_formatter.py @@ -1,8 +1,9 @@ import json -from app.definitions import Value, SurveyMetadata, PCK, SPP -from app.formatters.looping_formatter import LoopingFormatter -from app.formatters.spp_formatter import SPPFormatter +from app.definitions.input import SurveyMetadata, Value +from app.definitions.output import SPP, PCK +from app.services.formatters.looping_formatter import LoopingFormatter +from app.services.formatters.spp_formatter import SPPFormatter class SPPLoopingFormatter(SPPFormatter, LoopingFormatter): diff --git a/app/services/mappers/__init__.py b/app/services/mappers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/mappers/formatter_mappings.py b/app/services/mappers/formatter_mappings.py new file mode 100644 index 0000000..f5a199c --- /dev/null +++ b/app/services/mappers/formatter_mappings.py @@ -0,0 +1,17 @@ +from app.definitions.formatter import FormatterBase +from app.definitions.mapper import FormatterMappingBase +from app.definitions.spec import BuildSpecError +from app.services.mappers.formatter_selectors import FormatterSelector + + +class FormatterMapping[F: FormatterBase](FormatterMappingBase[F]): + + def __init__(self, mappings: dict[str, FormatterSelector]): + super().__init__(mappings) + + def get_formatter(self, target: str, looped: bool = False) -> F.__class__: + formatter: F.__class__ = self.get(target, looped) + if not formatter: + raise BuildSpecError(f"Could not find formatter for {target}") + + return formatter diff --git a/app/services/mappers/formatter_selectors.py b/app/services/mappers/formatter_selectors.py new file mode 100644 index 0000000..040e45f --- /dev/null +++ b/app/services/mappers/formatter_selectors.py @@ -0,0 +1,20 @@ +from typing import Optional + +from app.definitions.mapper import Selector +from app.services.formatters.formatter import Formatter +from app.services.formatters.looping_formatter import LoopingFormatter + + +class FormatterSelector(Selector[bool, Formatter.__class__]): + + def __init__(self, + formatter: Formatter.__class__, + looped_formatter: Optional[LoopingFormatter.__class__] = None): + self._formatter = formatter + self._looping_formatter = looped_formatter + + def choose(self, looped: bool = False) -> Formatter.__class__: + if looped and self._looping_formatter: + return self._looping_formatter + else: + return self._formatter diff --git a/app/services/mappers/spec_mappings.py b/app/services/mappers/spec_mappings.py new file mode 100644 index 0000000..ac7f860 --- /dev/null +++ b/app/services/mappers/spec_mappings.py @@ -0,0 +1,26 @@ +from app.definitions.mapper import BuildSpecMappingBase, Selector, PrepopMappingBase +from app.definitions.repository import BuildSpecRepositoryBase +from app.definitions.spec import BuildSpec +from app.definitions.input import SurveyMetadata + + +class BuildSpecMapping(BuildSpecMappingBase): + + def __init__(self, mappings: dict[str, Selector[SurveyMetadata, str]], repository: BuildSpecRepositoryBase): + super().__init__(mappings) + self._repository = repository + + def get_build_spec(self, survey_metadata: SurveyMetadata) -> BuildSpec: + spec_name: str = self.get(survey_metadata["survey_id"], survey_metadata) + return self._repository.get_build_spec(spec_name) + + +class PrepopSpecMapping(PrepopMappingBase): + + def __init__(self, mappings: dict[str, Selector[str, str]], repository: BuildSpecRepositoryBase): + super().__init__(mappings) + self._repository = repository + + def get_build_spec(self, survey_id: str) -> BuildSpec: + spec_name: str = self.get(survey_id, survey_id) + return self._repository.get_build_spec(spec_name) diff --git a/app/services/mappers/spec_selectors.py b/app/services/mappers/spec_selectors.py new file mode 100644 index 0000000..1996996 --- /dev/null +++ b/app/services/mappers/spec_selectors.py @@ -0,0 +1,39 @@ +from app.definitions.mapper import Selector +from app.definitions.input import SurveyMetadata +from app.services.period.period import Period + + +class PrepopSelector(Selector[str, str]): + def __init__(self, spec_name: str): + self._spec_name = spec_name + + def choose(self, _: str = "") -> str: + return self._spec_name + + +class BuildSpecSelector(Selector[SurveyMetadata, str]): + + def __init__(self, spec_name: str): + self._spec_name = spec_name + + def choose(self, survey_metadata: SurveyMetadata) -> str: + return self._spec_name + + +class BuildSpecPeriodSelector(BuildSpecSelector): + + def __init__(self, period_id, before: str, after_or_equal: str): + super().__init__(before) + self._period_id = period_id + self._before = before + self._after_or_equal = after_or_equal + + def choose(self, survey_metadata: SurveyMetadata) -> str: + period = Period(survey_metadata["period_id"]) + spec_name: str + if period < Period(self._period_id): + spec_name = self._before + else: + spec_name = self._after_or_equal + + return spec_name diff --git a/app/services/period/__init__.py b/app/services/period/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/period/period.py b/app/services/period/period.py new file mode 100644 index 0000000..9afc089 --- /dev/null +++ b/app/services/period/period.py @@ -0,0 +1,99 @@ +from typing import Optional + + +class PeriodFormatError(Exception): + pass + + +class Period: + + def __init__(self, period_id: str, period_format: Optional[str] = None): + self._period_id = period_id + if period_format is None or len(period_format) != len(period_id): + if len(period_id) == 6: + pf = "YYYYMM" + elif len(period_id) == 4: + pf = "YYMM" + elif len(period_id) == 2: + pf = "YY" + else: + raise PeriodFormatError(f"Period in wrong format {period_id}") + self._period_format = pf + else: + self._period_format = period_format + + def get_format(self) -> str: + return self._period_format + + def convert_to_format(self, new_format: str) -> str: + period = self._period_id + current_format = self._period_format + + symbols: dict[str, list[str]] = { + "D": [], + "M": [], + "Y": [] + } + + for i in range(0, len(period)): + k = current_format[i] + if k not in symbols: + raise PeriodFormatError(f"Period in wrong format {current_format}") + symbols[k].append(period[i]) + + if new_format.count("Y") == 4: + if len(symbols["Y"]) == 2: + symbols["Y"].insert(0, "0") + symbols["Y"].insert(0, "2") + elif new_format.count("Y") == 2: + if len(symbols["Y"]) == 4: + symbols["Y"].pop(0) + symbols["Y"].pop(0) + + if new_format.count("M") == 2: + if len(symbols["M"]) == 0: + symbols["M"].append("0") + symbols["M"].append("1") + + if new_format.count("D") == 2: + if len(symbols["D"]) == 0: + symbols["D"].append("0") + symbols["D"].append("1") + + result = "" + for f in new_format: + result += symbols[f].pop(0) + + return result + + def convert_to_YYYYMMDD(self) -> str: + return self.convert_to_format("YYYYMMDD") + + def are_comparable(self, other) -> bool: + if set(self._period_format) != set(other.get_format()): + return False + return True + + def __ge__(self, other): + if not self.are_comparable(other): + raise PeriodFormatError(f"Periods are not comparable {self._period_format} {other.get_format()}") + + return self.convert_to_YYYYMMDD() >= other.convert_to_YYYYMMDD() + + def __gt__(self, other): + if not self.are_comparable(other): + raise PeriodFormatError(f"Periods are not comparable {self._period_format} {other.get_format()}") + + return self.convert_to_YYYYMMDD() > other.convert_to_YYYYMMDD() + + def __le__(self, other): + if not self.are_comparable(other): + raise PeriodFormatError(f"Periods are not comparable {self._period_format} {other.get_format()}") + + return self.convert_to_YYYYMMDD() <= other.convert_to_YYYYMMDD() + + def __lt__(self, other): + if not self.are_comparable(other): + raise PeriodFormatError(f"Periods are not comparable {self._period_format} {other.get_format()}") + + return self.convert_to_YYYYMMDD() < other.convert_to_YYYYMMDD() diff --git a/app/services/transform/__init__.py b/app/services/transform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/transform/clean.py b/app/services/transform/clean.py similarity index 85% rename from app/transform/clean.py rename to app/services/transform/clean.py index d9cb6f5..c2383c9 100644 --- a/app/transform/clean.py +++ b/app/services/transform/clean.py @@ -1,7 +1,8 @@ from typing import final -from app.definitions import Template, Field, Empty -from app.transform.tree_walker import TreeWalker +from app.definitions.spec import Template +from app.definitions.input import Empty, Field +from app.services.transform.tree_walker import TreeWalker BLACK_LIST: final = ["", Empty, [], {}] diff --git a/app/services/transform/execute.py b/app/services/transform/execute.py new file mode 100644 index 0000000..a8daff6 --- /dev/null +++ b/app/services/transform/execute.py @@ -0,0 +1,77 @@ +from collections.abc import Callable +from typing import Final + +from sdx_gcp.app import get_logger + +from app.definitions.executor import ExecutorBase +from app.definitions.spec import ParseTree, Transform, BuildSpecError, Template, Transforms +from app.definitions.input import Value, Field, Data +from app.services.transform.interpolate import interpolate +from app.services.transform.populate import populate_mappings +from app.services.transform.tree_walker import TreeWalker + +logger = get_logger() + + +DERIVED_PREFIX: Final[str] = "&" +CURRENT_VALUE: Final[str] = DERIVED_PREFIX + "value" + + +class Executor(ExecutorBase): + + def __init__(self, function_lookup: dict[str, Callable]): + self._function_lookup = function_lookup + + def interpolate(self, template: Template, transforms: Transforms) -> ParseTree: + return interpolate(template, transforms) + + def populate(self, tree: ParseTree, data: Data) -> ParseTree: + return populate_mappings(tree, data) + + def execute(self, tree: ParseTree) -> dict[str, Value]: + """ + Convert a ParseTree into a dict of values by + performing each transform as a function. + The functions are looked up by name and then executed + in reverse order (from leaf to root in the tree), so that each invocation + either provides a value for its parent, or is the resulting value to be returned. + """ + + execute_transform = self._execute_transform + + class ExecuteTreeWalker(TreeWalker): + + def on_dict(self, name: str, field: dict[str, Field]) -> Field: + if 'name' in field.keys() and 'args' in field.keys(): + return execute_transform(field, self) + + return super().on_dict(name, field) + + def on_leaf(_name: str, field: str, walker: ExecuteTreeWalker) -> Field: + if field.startswith(DERIVED_PREFIX) and field != CURRENT_VALUE: + return walker.read_from_current(field[1:]) + + return field + + return ExecuteTreeWalker(tree=tree, on_str=on_leaf).walk_tree() + + def _execute_transform(self, transform: Transform, walker: TreeWalker) -> Value | list[Value]: + name = transform["name"] + f = self._function_lookup.get(name) + if f is None: + raise BuildSpecError(f"Transform name {name} is not a valid function!") + + args = transform["args"] + expanded_args: dict[str, Value] = {} + for arg_name, arg_val in args.items(): + expanded_args[arg_name] = walker.evaluate_field(arg_name, arg_val) + + derived_args: dict[str, Value] = {} + value: Value = expanded_args["value"] + for expanded_name, expanded_val in expanded_args.items(): + if expanded_val == CURRENT_VALUE: + derived_args[expanded_name] = value + elif expanded_name != "value": + derived_args[expanded_name] = expanded_val + + return f(value, **derived_args) diff --git a/app/services/transform/functions/__init__.py b/app/services/transform/functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/transform/functions/compound.py b/app/services/transform/functions/compound.py similarity index 81% rename from app/transform/functions/compound.py rename to app/services/transform/functions/compound.py index da55065..5efc529 100644 --- a/app/transform/functions/compound.py +++ b/app/services/transform/functions/compound.py @@ -1,6 +1,6 @@ -from app.definitions import Value, Empty -from app.transform.functions.numerical import round_half_up, divide -from app.transform.functions.time import to_date, ISO_8601_FORMAT, PCK_DATE_FORMAT, EQ_DATETIME_FORMAT +from app.definitions.input import Empty, Value +from app.services.transform.functions.numerical import round_half_up, divide +from app.services.transform.functions.time import to_date, ISO_8601_FORMAT, PCK_DATE_FORMAT, EQ_DATETIME_FORMAT def currency_thousands(value: Value) -> Value: diff --git a/app/transform/functions/general.py b/app/services/transform/functions/general.py similarity index 90% rename from app/transform/functions/general.py rename to app/services/transform/functions/general.py index 41f94a2..d10e917 100644 --- a/app/transform/functions/general.py +++ b/app/services/transform/functions/general.py @@ -1,6 +1,6 @@ from typing import Callable -from app.definitions import Value, Empty +from app.definitions.input import Empty, Value """ This file contains the functions that represent @@ -27,7 +27,7 @@ def any_exists(value: Value, values: list[Value], on_true: str = "1", on_false: return on_false -def lookup(value: Value, on_no_match: str = Empty, **kwargs: str) -> Value: +def lookup(value: Value, on_no_match: str | Empty = Empty, **kwargs: str) -> Value: if value in kwargs: return kwargs[value] return on_no_match diff --git a/app/transform/functions/lists.py b/app/services/transform/functions/lists.py similarity index 93% rename from app/transform/functions/lists.py rename to app/services/transform/functions/lists.py index 3ba2eeb..15bffda 100644 --- a/app/transform/functions/lists.py +++ b/app/services/transform/functions/lists.py @@ -1,4 +1,4 @@ -from app.definitions import Value, Empty +from app.definitions.input import Empty, Value """ This file contains the functions that represent diff --git a/app/transform/functions/numerical.py b/app/services/transform/functions/numerical.py similarity index 97% rename from app/transform/functions/numerical.py rename to app/services/transform/functions/numerical.py index 5194768..5f3c4d4 100644 --- a/app/transform/functions/numerical.py +++ b/app/services/transform/functions/numerical.py @@ -1,7 +1,7 @@ from collections.abc import Callable from decimal import Decimal, ROUND_HALF_UP -from app.definitions import Value, Empty +from app.definitions.input import Empty, Value """ This file contains the functions that represent diff --git a/app/transform/functions/string.py b/app/services/transform/functions/string.py similarity index 96% rename from app/transform/functions/string.py rename to app/services/transform/functions/string.py index 0d1f833..1ad20e7 100644 --- a/app/transform/functions/string.py +++ b/app/services/transform/functions/string.py @@ -1,8 +1,8 @@ import math import re -from app.definitions import Value -from app.transform.functions.general import handle_empties +from app.definitions.input import Value +from app.services.transform.functions.general import handle_empties """ This file contains the functions that represent diff --git a/app/transform/functions/time.py b/app/services/transform/functions/time.py similarity index 96% rename from app/transform/functions/time.py rename to app/services/transform/functions/time.py index 51326a8..73b9bba 100644 --- a/app/transform/functions/time.py +++ b/app/services/transform/functions/time.py @@ -3,8 +3,8 @@ from datetime import datetime, date from typing import Final -from app.definitions import Value, Empty -from app.transform.functions.general import handle_empties +from app.definitions.input import Empty, Value +from app.services.transform.functions.general import handle_empties EQ_DATETIME_FORMAT: Final[str] = "DD/MM/YYYY" diff --git a/app/transform/interpolate.py b/app/services/transform/interpolate.py similarity index 94% rename from app/transform/interpolate.py rename to app/services/transform/interpolate.py index b9d1109..80294fa 100644 --- a/app/transform/interpolate.py +++ b/app/services/transform/interpolate.py @@ -3,8 +3,9 @@ from sdx_gcp.app import get_logger -from app.definitions import Template, Transforms, ParseTree, Field, BuildSpecError -from app.transform.tree_walker import TreeWalker +from app.definitions.spec import Template, Transforms, ParseTree, BuildSpecError +from app.definitions.input import Field +from app.services.transform.tree_walker import TreeWalker logger = get_logger() diff --git a/app/transform/populate.py b/app/services/transform/populate.py similarity index 95% rename from app/transform/populate.py rename to app/services/transform/populate.py index 9aff700..a4cfa08 100644 --- a/app/transform/populate.py +++ b/app/services/transform/populate.py @@ -3,8 +3,9 @@ from sdx_gcp.app import get_logger -from app.definitions import ParseTree, Field, Data -from app.transform.tree_walker import TreeWalker +from app.definitions.spec import ParseTree +from app.definitions.input import Data, Field +from app.services.transform.tree_walker import TreeWalker logger = get_logger() diff --git a/app/transform/tree_walker.py b/app/services/transform/tree_walker.py similarity index 95% rename from app/transform/tree_walker.py rename to app/services/transform/tree_walker.py index d7059cb..1731b3e 100644 --- a/app/transform/tree_walker.py +++ b/app/services/transform/tree_walker.py @@ -1,7 +1,8 @@ from collections.abc import Callable from typing import Self -from app.definitions import ParseTree, Field +from app.definitions.spec import ParseTree +from app.definitions.input import Field class TreeWalker: diff --git a/app/transform/execute.py b/app/transform/execute.py deleted file mode 100644 index 9a23f3e..0000000 --- a/app/transform/execute.py +++ /dev/null @@ -1,104 +0,0 @@ -from collections.abc import Callable -from typing import Final - -from sdx_gcp.app import get_logger - -from app.definitions import ParseTree, Transform, Field, Value, BuildSpecError -from app.transform.functions.compound import currency_thousands, period_start, period_end -from app.transform.functions.general import no_transform, exists, any_exists, lookup -from app.transform.functions.lists import as_list, append_to_list, prepend_to_list, trim_list -from app.transform.functions.numerical import round_half_up, aggregate, mean, number_equals, total, divide -from app.transform.functions.string import starts_with, contains, any_contains, concat, carve, string_padding, \ - space_split, postcode_start, postcode_end -from app.transform.functions.time import to_date, any_date, start_of_month, end_of_month, start_of_year, end_of_year -from app.transform.tree_walker import TreeWalker - -logger = get_logger() - - -DERIVED_PREFIX: Final[str] = "&" -CURRENT_VALUE: Final[str] = DERIVED_PREFIX + "value" - - -_function_lookup: dict[str, Callable] = { - "VALUE": no_transform, - "EXISTS": exists, - "ANY_EXISTS": any_exists, - "LOOKUP": lookup, - "STARTS_WITH": starts_with, - "CONTAINS": contains, - "ANY_CONTAINS": any_contains, - "CONCAT": concat, - "TO_DATE": to_date, - "ANY_DATE": any_date, - "START_OF_MONTH": start_of_month, - "END_OF_MONTH": end_of_month, - "START_OF_YEAR": start_of_year, - "END_OF_YEAR": end_of_year, - "ROUND": round_half_up, - "TOTAL": total, - "DIVIDE": divide, - "AGGREGATE": aggregate, - "MEAN": mean, - "NUMBER_EQUALS": number_equals, - "CURRENCY_THOUSANDS": currency_thousands, - "PERIOD_START": period_start, - "PERIOD_END": period_end, - "CARVE": carve, - "AS_LIST": as_list, - "APPEND_TO_LIST": append_to_list, - "PREPEND_TO_LIST": prepend_to_list, - "TRIM_LIST": trim_list, - "PADDING": string_padding, - "SPACE_SPLIT": space_split, - "POSTCODE_START": postcode_start, - "POSTCODE_END": postcode_end, -} - - -def execute(tree: ParseTree) -> dict[str, Value]: - """ - Convert a ParseTree into a dict of values by - performing each transform as a function. - The functions are looked up by name and then executed - in reverse order (from leaf to root in the tree), so that each invocation - either provides a value for its parent, or is the resulting value to be returned. - """ - - class ExecuteTreeWalker(TreeWalker): - - def on_dict(self, name: str, field: dict[str, Field]) -> Field: - if 'name' in field.keys() and 'args' in field.keys(): - return execute_transform(field, self) - - return super().on_dict(name, field) - - def on_leaf(_name: str, field: str, walker: ExecuteTreeWalker) -> Field: - if field.startswith(DERIVED_PREFIX) and field != CURRENT_VALUE: - return walker.read_from_current(field[1:]) - - return field - - return ExecuteTreeWalker(tree=tree, on_str=on_leaf).walk_tree() - - -def execute_transform(transform: Transform, walker: TreeWalker) -> Value | list[Value]: - name = transform["name"] - f = _function_lookup.get(name) - if f is None: - raise BuildSpecError(f"Transform name {name} is not a valid function!") - - args = transform["args"] - expanded_args: dict[str, Value] = {} - for arg_name, arg_val in args.items(): - expanded_args[arg_name] = walker.evaluate_field(arg_name, arg_val) - - derived_args: dict[str, Value] = {} - value: Value = expanded_args["value"] - for expanded_name, expanded_val in expanded_args.items(): - if expanded_val == CURRENT_VALUE: - derived_args[expanded_name] = value - elif expanded_name != "value": - derived_args[expanded_name] = expanded_val - - return f(value, **derived_args) diff --git a/app/transformers/flat.py b/app/transformers/flat.py new file mode 100644 index 0000000..2e4f72b --- /dev/null +++ b/app/transformers/flat.py @@ -0,0 +1,10 @@ +from app.definitions.input import SurveyMetadata +from app.definitions.spec import BuildSpec +from app.services.formatters.formatter import Formatter +from app.transformers.spec import SpecTransformer + + +class FlatSpecTransformer(SpecTransformer[SurveyMetadata, Formatter]): + + def _load(self, survey_metadata: SurveyMetadata) -> BuildSpec: + return self._spec_mapping.get_build_spec(survey_metadata) diff --git a/app/transformers/looped.py b/app/transformers/looped.py new file mode 100644 index 0000000..9a85fd3 --- /dev/null +++ b/app/transformers/looped.py @@ -0,0 +1,29 @@ +from typing import Literal + +from app.definitions.input import SurveyMetadata +from app.definitions.spec import BuildSpec, ParseTree +from app.services.formatters.looping_formatter import LoopingFormatter +from app.services.transform.populate import resolve_value_fields +from app.transformers.spec import SpecTransformer + +template_type = Literal["template", "looped"] + + +class LoopedSpecTransformer(SpecTransformer[SurveyMetadata, LoopingFormatter]): + + def _load(self, survey_metadata: SurveyMetadata) -> BuildSpec: + self.looped = True + return self._spec_mapping.get_build_spec(survey_metadata) + + def interpolate_looped(self) -> ParseTree: + build_spec = self._build_spec + + template: template_type = "looped" + if "looped" not in build_spec: + template = "template" + + if 'transforms' in build_spec: + parse_tree: ParseTree = self._executor.interpolate(build_spec[template], build_spec["transforms"]) + else: + parse_tree: ParseTree = build_spec[template] + return resolve_value_fields(parse_tree) diff --git a/app/transformers/prepop.py b/app/transformers/prepop.py new file mode 100644 index 0000000..372482b --- /dev/null +++ b/app/transformers/prepop.py @@ -0,0 +1,8 @@ +from app.definitions.spec import BuildSpec +from app.transformers.spec import SpecTransformer + + +class PrepopTransformer(SpecTransformer[str, None]): + + def _load(self, survey_id: str) -> BuildSpec: + return self._spec_mapping.get_build_spec(survey_id) diff --git a/app/transformers/spec.py b/app/transformers/spec.py new file mode 100644 index 0000000..7de6d0b --- /dev/null +++ b/app/transformers/spec.py @@ -0,0 +1,56 @@ +from abc import ABC, abstractmethod + +from app.definitions.input import Data, Value +from app.definitions.executor import ExecutorBase +from app.definitions.formatter import FormatterBase +from app.definitions.mapper import SpecMappingBase, FormatterMappingBase +from app.definitions.spec import BuildSpec, ParseTree, BuildSpecError +from app.definitions.transformer import TransformerBase +from app.services.transform.populate import resolve_value_fields + + +class SpecTransformer[S, F: FormatterBase](TransformerBase[F], ABC): + + def __init__(self, + s: S, + spec_mapping: SpecMappingBase[S], + executor: ExecutorBase, + formatter_mapping: FormatterMappingBase[F]): + + self._spec_mapping = spec_mapping + self._executor = executor + self._formatter_mapping = formatter_mapping + self.looped = False + self._build_spec: BuildSpec = self._load(s) + + @abstractmethod + def _load(self, s: S) -> BuildSpec: + pass + + def get_spec(self) -> BuildSpec: + return self._build_spec + + def interpolate(self) -> ParseTree: + build_spec = self._build_spec + if 'transforms' in build_spec: + parse_tree: ParseTree = self._executor.interpolate(build_spec["template"], build_spec["transforms"]) + else: + parse_tree: ParseTree = build_spec["template"] + return resolve_value_fields(parse_tree) + + def run(self, tree: ParseTree, data: Data) -> dict[str, Value]: + populated_tree = self._executor.populate(tree, data) + return self._executor.execute(populated_tree) + + def get_formatter(self) -> F: + build_spec = self._build_spec + f: F.__class__ = self._formatter_mapping.get_formatter(build_spec["target"], self.looped) + if f is None: + raise BuildSpecError(f"Unable to find formatter for target: {build_spec['target']}") + + period_format = build_spec["period_format"] + pck_period_format = build_spec["pck_period_format"] if "pck_period_format" in build_spec else period_format + form_mapping = build_spec["form_mapping"] if "form_mapping" in build_spec else {} + + formatter: F = f(build_spec["period_format"], pck_period_format, form_mapping) + return formatter diff --git a/build_specs/pck/abs.yaml b/build_specs/abs.yaml similarity index 100% rename from build_specs/pck/abs.yaml rename to build_specs/abs.yaml diff --git a/build_specs/pck/acas.json b/build_specs/acas.json similarity index 100% rename from build_specs/pck/acas.json rename to build_specs/acas.json diff --git a/build_specs/pck/berd.yaml b/build_specs/berd.yaml similarity index 100% rename from build_specs/pck/berd.yaml rename to build_specs/berd.yaml diff --git a/build_specs/pck/blocks.yaml b/build_specs/blocks.yaml similarity index 100% rename from build_specs/pck/blocks.yaml rename to build_specs/blocks.yaml diff --git a/build_specs/prepop/bres.yaml b/build_specs/bres-prepop.yaml similarity index 100% rename from build_specs/prepop/bres.yaml rename to build_specs/bres-prepop.yaml diff --git a/build_specs/looping/bres.yaml b/build_specs/bres.yaml similarity index 100% rename from build_specs/looping/bres.yaml rename to build_specs/bres.yaml diff --git a/build_specs/pck/bricks.json b/build_specs/bricks.json similarity index 100% rename from build_specs/pck/bricks.json rename to build_specs/bricks.json diff --git a/build_specs/prepop/brs.yaml b/build_specs/brs-prepop.yaml similarity index 100% rename from build_specs/prepop/brs.yaml rename to build_specs/brs-prepop.yaml diff --git a/build_specs/construction-spp.yaml b/build_specs/construction-spp.yaml new file mode 100644 index 0000000..a0b58ef --- /dev/null +++ b/build_specs/construction-spp.yaml @@ -0,0 +1,30 @@ +title: "Build Spec for Construction" +survey_id: "228" +target: "SPP" +period_format: "YYMM" +template: + "1": "#1" + "11": "#11" + "12": "#12" + "50": "#50" + "51": "#51" + "52": "#52" + "53": "#53" + "54": "#54" + "146": "#146" + "201": "#201" + "202": "#202" + "211": "#211" + "212": "#212" + "221": "#221" + "222": "#222" + "231": "#231" + "232": "#232" + "241": "#241" + "242": "#242" + "243": "#243" + "290": "#290" + "901": "#901" + "902": "#902" + "903": "#903" + "904": "#904" diff --git a/build_specs/pck/construction.yaml b/build_specs/construction.yaml similarity index 100% rename from build_specs/pck/construction.yaml rename to build_specs/construction.yaml diff --git a/build_specs/pck/des.yaml b/build_specs/des.yaml similarity index 100% rename from build_specs/pck/des.yaml rename to build_specs/des.yaml diff --git a/build_specs/pck/fuels.yaml b/build_specs/fuels.yaml similarity index 100% rename from build_specs/pck/fuels.yaml rename to build_specs/fuels.yaml diff --git a/build_specs/prepop/land.yaml b/build_specs/land-prepop.yaml similarity index 100% rename from build_specs/prepop/land.yaml rename to build_specs/land-prepop.yaml diff --git a/build_specs/looping/looping-spp.yaml b/build_specs/looping-spp.yaml similarity index 100% rename from build_specs/looping/looping-spp.yaml rename to build_specs/looping-spp.yaml diff --git a/build_specs/looping/looping.yaml b/build_specs/looping.yaml similarity index 100% rename from build_specs/looping/looping.yaml rename to build_specs/looping.yaml diff --git a/build_specs/looping/qrt.yaml b/build_specs/looping/qrt.yaml deleted file mode 100644 index 691a27f..0000000 --- a/build_specs/looping/qrt.yaml +++ /dev/null @@ -1,6 +0,0 @@ -title: "Build spec for Roofing Tiles" -survey_id: "068" -target: "Image" -period_format: "YYYYMM" -template: {} -transforms: {} \ No newline at end of file diff --git a/build_specs/looping/qs.yaml b/build_specs/looping/qs.yaml deleted file mode 100644 index 5ba9a34..0000000 --- a/build_specs/looping/qs.yaml +++ /dev/null @@ -1,6 +0,0 @@ -title: "Build spec for Slate" -survey_id: "071" -target: "Image" -period_format: "YYYYMM" -template: {} -transforms: {} \ No newline at end of file diff --git a/build_specs/prepop/marine.yaml b/build_specs/marine-prepop.yaml similarity index 100% rename from build_specs/prepop/marine.yaml rename to build_specs/marine-prepop.yaml diff --git a/build_specs/mbs-spp.yaml b/build_specs/mbs-spp.yaml new file mode 100644 index 0000000..d7c4b27 --- /dev/null +++ b/build_specs/mbs-spp.yaml @@ -0,0 +1,23 @@ +title: "Build Spec for Monthly Business Survey" +survey_id: "009" +target: "SPP" +period_format: "YYMM" +pck_period_format: "YYMM" + +template: + "11": "#11" + "12": "#12" + "40": "#40" + "42": "#42" + "43": "#43" + "46": "#46" + "47": "#47" + "49": "#49" + "50": "#50" + "51": "#51" + "52": "#52" + "53": "#53" + "54": "#54" + "90": "#90" + "110": "#110" + "146": "#146" diff --git a/build_specs/pck/mbs.yaml b/build_specs/mbs.yaml similarity index 100% rename from build_specs/pck/mbs.yaml rename to build_specs/mbs.yaml diff --git a/build_specs/pck/mcg.yaml b/build_specs/mcg.yaml similarity index 100% rename from build_specs/pck/mcg.yaml rename to build_specs/mcg.yaml diff --git a/build_specs/pck/mes.yaml b/build_specs/mes.yaml similarity index 100% rename from build_specs/pck/mes.yaml rename to build_specs/mes.yaml diff --git a/build_specs/pck/mwss.yaml b/build_specs/mwss.yaml similarity index 100% rename from build_specs/pck/mwss.yaml rename to build_specs/mwss.yaml diff --git a/build_specs/qbs-spp.yaml b/build_specs/qbs-spp.yaml new file mode 100644 index 0000000..48e939a --- /dev/null +++ b/build_specs/qbs-spp.yaml @@ -0,0 +1,13 @@ +title: "Quarterly Business Survey (QBS)" +survey_id: "139" +target: "SPP" +period_format: "YYMM" + +template: + "1": "#1" + "50": "#50" + "51": "#51" + "52": "#52" + "53": "#53" + "54": "#54" + "146": "#146" diff --git a/build_specs/pck/qbs.yaml b/build_specs/qbs.yaml similarity index 100% rename from build_specs/pck/qbs.yaml rename to build_specs/qbs.yaml diff --git a/build_specs/pck/qcas.yaml b/build_specs/qcas.yaml similarity index 100% rename from build_specs/pck/qcas.yaml rename to build_specs/qcas.yaml diff --git a/build_specs/pck/qpses.yaml b/build_specs/qpses.yaml similarity index 100% rename from build_specs/pck/qpses.yaml rename to build_specs/qpses.yaml diff --git a/build_specs/pck/qpsespb.yaml b/build_specs/qpsespb.yaml similarity index 100% rename from build_specs/pck/qpsespb.yaml rename to build_specs/qpsespb.yaml diff --git a/build_specs/pck/qpsesrap.yaml b/build_specs/qpsesrap.yaml similarity index 100% rename from build_specs/pck/qpsesrap.yaml rename to build_specs/qpsesrap.yaml diff --git a/build_specs/looping/qsl.yaml b/build_specs/qsl.yaml similarity index 100% rename from build_specs/looping/qsl.yaml rename to build_specs/qsl.yaml diff --git a/build_specs/looping/qsm.yaml b/build_specs/qsm.yaml similarity index 100% rename from build_specs/looping/qsm.yaml rename to build_specs/qsm.yaml diff --git a/build_specs/pck/rails.yaml b/build_specs/rails.yaml similarity index 100% rename from build_specs/pck/rails.yaml rename to build_specs/rails.yaml diff --git a/build_specs/prepop/slate.yaml b/build_specs/slate-prepop.yaml similarity index 100% rename from build_specs/prepop/slate.yaml rename to build_specs/slate-prepop.yaml diff --git a/build_specs/pck/stocks.yaml b/build_specs/stocks.yaml similarity index 100% rename from build_specs/pck/stocks.yaml rename to build_specs/stocks.yaml diff --git a/build_specs/prepop/tiles.yaml b/build_specs/tiles-prepop.yaml similarity index 100% rename from build_specs/prepop/tiles.yaml rename to build_specs/tiles-prepop.yaml diff --git a/build_specs/pck/ukis.yaml b/build_specs/ukis.yaml similarity index 100% rename from build_specs/pck/ukis.yaml rename to build_specs/ukis.yaml diff --git a/build_specs/pck/vacancies.yaml b/build_specs/vacancies.yaml similarity index 100% rename from build_specs/pck/vacancies.yaml rename to build_specs/vacancies.yaml diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 994291a..081b45f 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -1,11 +1,11 @@ steps: - id: 'Unit Tests' - name: 'europe-west2-docker.pkg.dev/ons-sdx-ci/sdx-apps/sdx-gcp:1.4.4' + name: 'europe-west2-docker.pkg.dev/ons-sdx-ci/sdx-apps/sdx-gcp:1.4.5' script: | #!/usr/bin/env bash + poetry export -f requirements.txt --output requirements.txt --with dev pip install -r requirements.txt - pip install -r test-requirements.txt flake8 . --count --statistics pytest -v --cov-report term-missing --disable-warnings --cov=app tests/ options: - defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET \ No newline at end of file + defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..6dc28cf --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1357 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "cachetools" +version = "5.5.1" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, + {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "coverage" +version = "7.6.12" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "deprecated" +version = "1.2.18" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] + +[[package]] +name = "flake8" +version = "7.1.2" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +groups = ["dev"] +files = [ + {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, + {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "flask" +version = "3.1.0" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, + {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, +] + +[package.dependencies] +blinker = ">=1.9" +click = ">=8.1.3" +itsdangerous = ">=2.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.1" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "google-api-core" +version = "2.24.1" +description = "Google API client core library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, + {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.dev0" +googleapis-common-protos = ">=1.56.2,<2.0.dev0" +grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +proto-plus = {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""} +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +requests = ">=2.18.0,<3.0.0.dev0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] + +[[package]] +name = "google-auth" +version = "2.38.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, + {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "google-cloud-core" +version = "2.4.1" +description = "Google Cloud API client core library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, + {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, +] + +[package.dependencies] +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" +google-auth = ">=1.25.0,<3.0dev" + +[package.extras] +grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] + +[[package]] +name = "google-cloud-datastore" +version = "2.20.2" +description = "Google Cloud Datastore API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_datastore-2.20.2-py2.py3-none-any.whl", hash = "sha256:d2190180343b807d4aa3b0b3bb837606349b71e5e74e29aa9009c0ae38c0b6a0"}, + {file = "google_cloud_datastore-2.20.2.tar.gz", hash = "sha256:9665d009729d9551329d9476f4d5bda9c11d3469243ea8a2c0d9490b65aa899f"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" +google-cloud-core = ">=1.4.0,<3.0.0dev" +proto-plus = {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""} +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" + +[package.extras] +libcst = ["libcst (>=0.2.5)"] + +[[package]] +name = "google-cloud-pubsub" +version = "2.27.3" +description = "Google Cloud Pub/Sub API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_pubsub-2.27.3-py2.py3-none-any.whl", hash = "sha256:6e294b063d6c6bf44d7b1ca99721ae6137930df3fdf7b91e44d280dd84b9410c"}, + {file = "google_cloud_pubsub-2.27.3.tar.gz", hash = "sha256:daa03d16552c34240774307fc69ceebb991a94d70d0d6f208179e375f503f532"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} +google-auth = ">=2.14.1,<3.0.0dev" +grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" +grpcio = ">=1.51.3,<2.0dev" +grpcio-status = ">=1.33.2" +opentelemetry-api = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} +opentelemetry-sdk = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} +proto-plus = {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""} +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" + +[package.extras] +libcst = ["libcst (>=0.3.10)"] + +[[package]] +name = "google-cloud-secret-manager" +version = "2.22.1" +description = "Google Cloud Secret Manager API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_secret_manager-2.22.1-py2.py3-none-any.whl", hash = "sha256:f18c5094d2d462a58d1ef23cd62895f68ad0adebf96f6b8612f35197a456908c"}, + {file = "google_cloud_secret_manager-2.22.1.tar.gz", hash = "sha256:f245e505b429990388f0f92cd4a24d424a4c4cdc2acb866e52c24e7680d15e77"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" +grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" +proto-plus = {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""} +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" + +[[package]] +name = "google-cloud-storage" +version = "2.19.0" +description = "Google Cloud Storage API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba"}, + {file = "google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2"}, +] + +[package.dependencies] +google-api-core = ">=2.15.0,<3.0.0dev" +google-auth = ">=2.26.1,<3.0dev" +google-cloud-core = ">=2.3.0,<3.0dev" +google-crc32c = ">=1.0,<2.0dev" +google-resumable-media = ">=2.7.2" +requests = ">=2.18.0,<3.0.0dev" + +[package.extras] +protobuf = ["protobuf (<6.0.0dev)"] +tracing = ["opentelemetry-api (>=1.1.0)"] + +[[package]] +name = "google-crc32c" +version = "1.6.0" +description = "A python wrapper of the C library 'Google CRC32C'" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa"}, + {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9"}, + {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7"}, + {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e"}, + {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc"}, + {file = "google_crc32c-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42"}, + {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4"}, + {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8"}, + {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d"}, + {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f"}, + {file = "google_crc32c-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3"}, + {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d"}, + {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b"}, + {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00"}, + {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3"}, + {file = "google_crc32c-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760"}, + {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205"}, + {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0"}, + {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2"}, + {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871"}, + {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57"}, + {file = "google_crc32c-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c"}, + {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc"}, + {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d"}, + {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24"}, + {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d"}, + {file = "google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc"}, +] + +[package.extras] +testing = ["pytest"] + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +description = "Utilities for Google Media Downloads and Resumable Uploads" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa"}, + {file = "google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0"}, +] + +[package.dependencies] +google-crc32c = ">=1.0,<2.0dev" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] +requests = ["requests (>=2.18.0,<3.0.0dev)"] + +[[package]] +name = "googleapis-common-protos" +version = "1.67.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "googleapis_common_protos-1.67.0-py2.py3-none-any.whl", hash = "sha256:579de760800d13616f51cf8be00c876f00a9f146d3e6510e19d1f4111758b741"}, + {file = "googleapis_common_protos-1.67.0.tar.gz", hash = "sha256:21398025365f138be356d5923e9168737d94d46a72aefee4a6110a1f23463c86"}, +] + +[package.dependencies] +grpcio = {version = ">=1.44.0,<2.0.0.dev0", optional = true, markers = "extra == \"grpc\""} +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.0" +description = "IAM API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "grpc_google_iam_v1-0.14.0-py2.py3-none-any.whl", hash = "sha256:fb4a084b30099ba3ab07d61d620a0d4429570b13ff53bd37bac75235f98b7da4"}, + {file = "grpc_google_iam_v1-0.14.0.tar.gz", hash = "sha256:c66e07aa642e39bb37950f9e7f491f70dad150ac9801263b42b2814307c2df99"}, +] + +[package.dependencies] +googleapis-common-protos = {version = ">=1.56.0,<2.0.0dev", extras = ["grpc"]} +grpcio = ">=1.44.0,<2.0.0dev" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" + +[[package]] +name = "grpcio" +version = "1.70.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "grpcio-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:95469d1977429f45fe7df441f586521361e235982a0b39e33841549143ae2851"}, + {file = "grpcio-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ed9718f17fbdb472e33b869c77a16d0b55e166b100ec57b016dc7de9c8d236bf"}, + {file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:374d014f29f9dfdb40510b041792e0e2828a1389281eb590df066e1cc2b404e5"}, + {file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2af68a6f5c8f78d56c145161544ad0febbd7479524a59c16b3e25053f39c87f"}, + {file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7df14b2dcd1102a2ec32f621cc9fab6695effef516efbc6b063ad749867295"}, + {file = "grpcio-1.70.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c78b339869f4dbf89881e0b6fbf376313e4f845a42840a7bdf42ee6caed4b11f"}, + {file = "grpcio-1.70.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58ad9ba575b39edef71f4798fdb5c7b6d02ad36d47949cd381d4392a5c9cbcd3"}, + {file = "grpcio-1.70.0-cp310-cp310-win32.whl", hash = "sha256:2b0d02e4b25a5c1f9b6c7745d4fa06efc9fd6a611af0fb38d3ba956786b95199"}, + {file = "grpcio-1.70.0-cp310-cp310-win_amd64.whl", hash = "sha256:0de706c0a5bb9d841e353f6343a9defc9fc35ec61d6eb6111802f3aa9fef29e1"}, + {file = "grpcio-1.70.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:17325b0be0c068f35770f944124e8839ea3185d6d54862800fc28cc2ffad205a"}, + {file = "grpcio-1.70.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:dbe41ad140df911e796d4463168e33ef80a24f5d21ef4d1e310553fcd2c4a386"}, + {file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5ea67c72101d687d44d9c56068328da39c9ccba634cabb336075fae2eab0d04b"}, + {file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb5277db254ab7586769e490b7b22f4ddab3876c490da0a1a9d7c695ccf0bf77"}, + {file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7831a0fc1beeeb7759f737f5acd9fdcda520e955049512d68fda03d91186eea"}, + {file = "grpcio-1.70.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:27cc75e22c5dba1fbaf5a66c778e36ca9b8ce850bf58a9db887754593080d839"}, + {file = "grpcio-1.70.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d63764963412e22f0491d0d32833d71087288f4e24cbcddbae82476bfa1d81fd"}, + {file = "grpcio-1.70.0-cp311-cp311-win32.whl", hash = "sha256:bb491125103c800ec209d84c9b51f1c60ea456038e4734688004f377cfacc113"}, + {file = "grpcio-1.70.0-cp311-cp311-win_amd64.whl", hash = "sha256:d24035d49e026353eb042bf7b058fb831db3e06d52bee75c5f2f3ab453e71aca"}, + {file = "grpcio-1.70.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:ef4c14508299b1406c32bdbb9fb7b47612ab979b04cf2b27686ea31882387cff"}, + {file = "grpcio-1.70.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:aa47688a65643afd8b166928a1da6247d3f46a2784d301e48ca1cc394d2ffb40"}, + {file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:880bfb43b1bb8905701b926274eafce5c70a105bc6b99e25f62e98ad59cb278e"}, + {file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e654c4b17d07eab259d392e12b149c3a134ec52b11ecdc6a515b39aceeec898"}, + {file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2394e3381071045a706ee2eeb6e08962dd87e8999b90ac15c55f56fa5a8c9597"}, + {file = "grpcio-1.70.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b3c76701428d2df01964bc6479422f20e62fcbc0a37d82ebd58050b86926ef8c"}, + {file = "grpcio-1.70.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac073fe1c4cd856ebcf49e9ed6240f4f84d7a4e6ee95baa5d66ea05d3dd0df7f"}, + {file = "grpcio-1.70.0-cp312-cp312-win32.whl", hash = "sha256:cd24d2d9d380fbbee7a5ac86afe9787813f285e684b0271599f95a51bce33528"}, + {file = "grpcio-1.70.0-cp312-cp312-win_amd64.whl", hash = "sha256:0495c86a55a04a874c7627fd33e5beaee771917d92c0e6d9d797628ac40e7655"}, + {file = "grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a"}, + {file = "grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429"}, + {file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9"}, + {file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c"}, + {file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f"}, + {file = "grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0"}, + {file = "grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40"}, + {file = "grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce"}, + {file = "grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68"}, + {file = "grpcio-1.70.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:8058667a755f97407fca257c844018b80004ae8035565ebc2812cc550110718d"}, + {file = "grpcio-1.70.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:879a61bf52ff8ccacbedf534665bb5478ec8e86ad483e76fe4f729aaef867cab"}, + {file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:0ba0a173f4feacf90ee618fbc1a27956bfd21260cd31ced9bc707ef551ff7dc7"}, + {file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558c386ecb0148f4f99b1a65160f9d4b790ed3163e8610d11db47838d452512d"}, + {file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:412faabcc787bbc826f51be261ae5fa996b21263de5368a55dc2cf824dc5090e"}, + {file = "grpcio-1.70.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3b0f01f6ed9994d7a0b27eeddea43ceac1b7e6f3f9d86aeec0f0064b8cf50fdb"}, + {file = "grpcio-1.70.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7385b1cb064734005204bc8994eed7dcb801ed6c2eda283f613ad8c6c75cf873"}, + {file = "grpcio-1.70.0-cp38-cp38-win32.whl", hash = "sha256:07269ff4940f6fb6710951116a04cd70284da86d0a4368fd5a3b552744511f5a"}, + {file = "grpcio-1.70.0-cp38-cp38-win_amd64.whl", hash = "sha256:aba19419aef9b254e15011b230a180e26e0f6864c90406fdbc255f01d83bc83c"}, + {file = "grpcio-1.70.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:4f1937f47c77392ccd555728f564a49128b6a197a05a5cd527b796d36f3387d0"}, + {file = "grpcio-1.70.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:0cd430b9215a15c10b0e7d78f51e8a39d6cf2ea819fd635a7214fae600b1da27"}, + {file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:e27585831aa6b57b9250abaf147003e126cd3a6c6ca0c531a01996f31709bed1"}, + {file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1af8e15b0f0fe0eac75195992a63df17579553b0c4af9f8362cc7cc99ccddf4"}, + {file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbce24409beaee911c574a3d75d12ffb8c3e3dd1b813321b1d7a96bbcac46bf4"}, + {file = "grpcio-1.70.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ff4a8112a79464919bb21c18e956c54add43ec9a4850e3949da54f61c241a4a6"}, + {file = "grpcio-1.70.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5413549fdf0b14046c545e19cfc4eb1e37e9e1ebba0ca390a8d4e9963cab44d2"}, + {file = "grpcio-1.70.0-cp39-cp39-win32.whl", hash = "sha256:b745d2c41b27650095e81dea7091668c040457483c9bdb5d0d9de8f8eb25e59f"}, + {file = "grpcio-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:a31d7e3b529c94e930a117b2175b2efd179d96eb3c7a21ccb0289a8ab05b645c"}, + {file = "grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.70.0)"] + +[[package]] +name = "grpcio-status" +version = "1.70.0" +description = "Status proto mapping for gRPC" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "grpcio_status-1.70.0-py3-none-any.whl", hash = "sha256:fc5a2ae2b9b1c1969cc49f3262676e6854aa2398ec69cb5bd6c47cd501904a85"}, + {file = "grpcio_status-1.70.0.tar.gz", hash = "sha256:0e7b42816512433b18b9d764285ff029bde059e9d41f8fe10a60631bd8348101"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.5.5" +grpcio = ">=1.70.0" +protobuf = ">=5.26.1,<6.0dev" + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.30.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09"}, + {file = "opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<=8.5.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.30.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091"}, + {file = "opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18"}, +] + +[package.dependencies] +opentelemetry-api = "1.30.0" +opentelemetry-semantic-conventions = "0.51b0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.51b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae"}, + {file = "opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +opentelemetry-api = "1.30.0" + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "proto-plus" +version = "1.26.0" +description = "Beautiful, Pythonic protocol buffers" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, + {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<6.0.0dev" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "5.29.3" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, + {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, + {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, + {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, + {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, + {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, + {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, + {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, + {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.7.0" + +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +groups = ["main"] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "sdx-gcp" +version = "1.4.4" +description = "" +optional = false +python-versions = ">=3.10,<4.0" +groups = ["main"] +files = [ + {file = "sdx_gcp-1.4.4-py3-none-any.whl", hash = "sha256:6297a8757b19f5674b1980b8508f1e0ccbaf90dffa7d8904202ec017b63882ea"}, + {file = "sdx_gcp-1.4.4.tar.gz", hash = "sha256:800b0fb95b37b511176ee10d564da7d8fb6a52d09647c282231ff5b19942d1f2"}, +] + +[package.dependencies] +flask = "3.1.0" +google-cloud-datastore = "2.20.2" +google-cloud-pubsub = "2.27.3" +google-cloud-secret-manager = "2.22.1" +google-cloud-storage = "2.19.0" +requests = "2.32.3" +setuptools = "75.8.0" +structlog = "25.1.0" +waitress = "3.0.2" + +[package.source] +type = "legacy" +url = "https://europe-west2-python.pkg.dev/ons-sdx-ci/sdx-python-packages/simple" +reference = "sdx-repo" + +[[package]] +name = "setuptools" +version = "75.8.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "structlog" +version = "25.1.0" +description = "Structured Logging for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "structlog-25.1.0-py3-none-any.whl", hash = "sha256:843fe4f254540329f380812cbe612e1af5ec5b8172205ae634679cd35a6d6321"}, + {file = "structlog-25.1.0.tar.gz", hash = "sha256:2ef2a572e0e27f09664965d31a576afe64e46ac6084ef5cec3c2b8cd6e4e3ad3"}, +] + +[package.extras] +dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "waitress" +version = "3.0.2" +description = "Waitress WSGI server" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"}, + {file = "waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"}, +] + +[package.extras] +docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] +testing = ["coverage (>=7.6.0)", "pytest", "pytest-cov"] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wrapt" +version = "1.17.2" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.13.0" +content-hash = "b6d22d4b42b41c1bda72b70a571bac4a6c51e841385d73fd0aa362b39e6e901b" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a7dec65 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "sdx-transformer" +version = "1.0.0" +description = "SDX Transformer" +authors = ["ONSDigital"] +readme = "README.md" +package-mode = false + + +[[tool.poetry.source]] +name = "sdx-repo" +url = "https://europe-west2-python.pkg.dev/ons-sdx-ci/sdx-python-packages/simple/" +priority = "explicit" + + +[tool.poetry.dependencies] +python = "^3.13.0" +sdx-gcp = { version = "1.4.4", source = "sdx-repo" } +PyYAML = "6.0.1" + + +[tool.poetry.group.dev.dependencies] +flake8 = "^7.1.0" +pytest = "^7.0" +pytest-cov = "^3.0" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 719e3ac..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sdx-gcp==1.4.4 -PyYAML~=6.0.1 \ No newline at end of file diff --git a/scripts/baby_jac.py b/scripts/baby_jac.py index fd061e2..29336d0 100644 --- a/scripts/baby_jac.py +++ b/scripts/baby_jac.py @@ -2,10 +2,10 @@ import yaml -from app.definitions import BuildSpec +from app.definitions.spec import BuildSpec -build_spec_path = "../build_specs/pck/abs.yaml" +build_spec_path = "../build_specs/abs.yaml" with open(build_spec_path) as y: build_spec: BuildSpec = yaml.safe_load(y.read()) diff --git a/temp.json b/temp.json deleted file mode 100644 index 9e2a520..0000000 --- a/temp.json +++ /dev/null @@ -1,95 +0,0 @@ -[ - { - "questioncode": "999", - "response": "Sand\nGravel, Pebbles, Shingle, Flint\nSand and gravel used for constructional fill", - "instance": 0 - }, - { - "questioncode": "601", - "response": "1", - "instance": 0 - }, - { - "questioncode": "602", - "response": "1", - "instance": 0 - }, - { - "questioncode": "603", - "response": "1", - "instance": 0 - }, - { - "questioncode": "604", - "response": "1", - "instance": 0 - }, - { - "questioncode": "605", - "response": "1", - "instance": 0 - }, - { - "questioncode": "606", - "response": "1", - "instance": 0 - }, - { - "questioncode": "607", - "response": "1", - "instance": 0 - }, - { - "questioncode": "146", - "response": "Lorem ipsum", - "instance": 0 - }, - { - "questioncode": "999", - "response": "Sand\nGravel, Pebbles, Shingle, Flint\nSand and gravel used for constructional fill", - "instance": 1, - "sd_identifier": "3340224" - }, - { - "questioncode": "601", - "response": "1", - "instance": 1, - "sd_identifier": "3340224" - }, - { - "questioncode": "602", - "response": "1", - "instance": 1, - "sd_identifier": "3340224" - }, - { - "questioncode": "603", - "response": "1", - "instance": 1, - "sd_identifier": "3340224" - }, - { - "questioncode": "604", - "response": "1", - "instance": 1, - "sd_identifier": "3340224" - }, - { - "questioncode": "605", - "response": "1", - "instance": 1, - "sd_identifier": "3340224" - }, - { - "questioncode": "606", - "response": "1", - "instance": 1, - "sd_identifier": "3340224" - }, - { - "questioncode": "607", - "response": "1", - "instance": 1, - "sd_identifier": "3340224" - } -] diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index d763507..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest==7.3.1 -pytest-cov==4.0.0 -flake8==6.0.0 -mypy==1.5.0 \ No newline at end of file diff --git a/tests/data/construction/228.0001-spp.json b/tests/data/construction/228.0001-spp.json new file mode 100644 index 0000000..684c022 --- /dev/null +++ b/tests/data/construction/228.0001-spp.json @@ -0,0 +1,93 @@ +{ + "formtype": "0001", + "reference": "48514665167", + "period": "2504", + "survey": "228", + "responses": [ + { + "questioncode": "146", + "response": "no comment!", + "instance": 0 + }, + { + "questioncode": "201", + "response": "8124.67", + "instance": 0 + }, + { + "questioncode": "202", + "response": "3321.54", + "instance": 0 + }, + { + "questioncode": "211", + "response": "3121.99", + "instance": 0 + }, + { + "questioncode": "212", + "response": "2887.32", + "instance": 0 + }, + { + "questioncode": "221", + "response": "7114.88", + "instance": 0 + }, + { + "questioncode": "222", + "response": "4014.87", + "instance": 0 + }, + { + "questioncode": "231", + "response": "3000", + "instance": 0 + }, + { + "questioncode": "232", + "response": "3555.55", + "instance": 0 + }, + { + "questioncode": "241", + "response": "4191.02", + "instance": 0 + }, + { + "questioncode": "242", + "response": "2200.33", + "instance": 0 + }, + { + "questioncode": "243", + "response": "1919.55", + "instance": 0 + }, + { + "questioncode": "290", + "response": "569234.33", + "instance": 0 + }, + { + "questioncode": "901", + "response": "Yes, I can report for this period", + "instance": 0 + }, + { + "questioncode": "902", + "response": "Yes, we carried out work on housing", + "instance": 0 + }, + { + "questioncode": "903", + "response": "Yes, we carried out work on infrastructure", + "instance": 0 + }, + { + "questioncode": "904", + "response": "Yes, we carried out other construction work", + "instance": 0 + } + ] +} diff --git a/tests/data/mbs/009.0106-spp.json b/tests/data/mbs/009.0106-spp.json new file mode 100644 index 0000000..1b91e31 --- /dev/null +++ b/tests/data/mbs/009.0106-spp.json @@ -0,0 +1,18 @@ +{ + "formtype": "0106", + "reference": "97148856319", + "period": "2503", + "survey": "009", + "responses": [ + { + "questioncode": "40", + "response": "24000", + "instance": 0 + }, + { + "questioncode": "146", + "response": "My significant comment", + "instance": 0 + } + ] +} diff --git a/tests/data/mbs/009.0111-spp.json b/tests/data/mbs/009.0111-spp.json new file mode 100644 index 0000000..2912cc4 --- /dev/null +++ b/tests/data/mbs/009.0111-spp.json @@ -0,0 +1,13 @@ +{ + "formtype": "0111", + "reference": "97148856319", + "period": "2503", + "survey": "009", + "responses": [ + { + "questioncode": "40", + "response": "1100", + "instance": 0 + } + ] +} diff --git a/tests/data/mbs/009.0255-spp.json b/tests/data/mbs/009.0255-spp.json new file mode 100644 index 0000000..68c79e6 --- /dev/null +++ b/tests/data/mbs/009.0255-spp.json @@ -0,0 +1,53 @@ +{ + "formtype": "0255", + "reference": "97148856319", + "period": "2503", + "survey": "009", + "responses": [ + { + "questioncode": "40", + "response": "81000", + "instance": 0 + }, + { + "questioncode": "49", + "response": "22000", + "instance": 0 + }, + { + "questioncode": "50", + "response": "67", + "instance": 0 + }, + { + "questioncode": "51", + "response": "15", + "instance": 0 + }, + { + "questioncode": "52", + "response": "15", + "instance": 0 + }, + { + "questioncode": "53", + "response": "15", + "instance": 0 + }, + { + "questioncode": "54", + "response": "22", + "instance": 0 + }, + { + "questioncode": "90", + "response": "35000", + "instance": 0 + }, + { + "questioncode": "146", + "response": "this is just a comment", + "instance": 0 + } + ] +} diff --git a/tests/data/mbs/009.0867-spp.json b/tests/data/mbs/009.0867-spp.json new file mode 100644 index 0000000..944e753 --- /dev/null +++ b/tests/data/mbs/009.0867-spp.json @@ -0,0 +1,48 @@ +{ + "formtype": "0867", + "reference": "97148856319", + "period": "2503", + "survey": "009", + "responses": [ + { + "questioncode": "11", + "response": "04/05/2016", + "instance": 0 + }, + { + "questioncode": "12", + "response": "04/06/2016", + "instance": 0 + }, + { + "questioncode": "40", + "response": "34000", + "instance": 0 + }, + { + "questioncode": "50", + "response": "23", + "instance": 0 + }, + { + "questioncode": "51", + "response": "20", + "instance": 0 + }, + { + "questioncode": "53", + "response": "1", + "instance": 0 + }, + { + "questioncode": "54", + "response": "2", + "instance": 0 + }, + { + "questioncode": "146", + "response": "My comment!", + "instance": 0 + } + ] +} diff --git a/tests/data/qbs/139.0001-spp.json b/tests/data/qbs/139.0001-spp.json new file mode 100644 index 0000000..6c130f8 --- /dev/null +++ b/tests/data/qbs/139.0001-spp.json @@ -0,0 +1,38 @@ +{ + "formtype": "0001", + "reference": "12345678901", + "period": "2503", + "survey": "139", + "responses": [ + { + "questioncode": "50", + "response": "10", + "instance": 0 + }, + { + "questioncode": "51", + "response": "1", + "instance": 0 + }, + { + "questioncode": "52", + "response": "2", + "instance": 0 + }, + { + "questioncode": "53", + "response": "3", + "instance": 0 + }, + { + "questioncode": "54", + "response": "4", + "instance": 0 + }, + { + "questioncode": "146", + "response": "This is a comment", + "instance": 0 + } + ] +} diff --git a/tests/integration/berd/test_berd_long.py b/tests/integration/berd/test_berd_long.py index c5511cb..d6aa532 100644 --- a/tests/integration/berd/test_berd_long.py +++ b/tests/integration/berd/test_berd_long.py @@ -1,39 +1,14 @@ import json import unittest -from app.definitions import SurveyMetadata, ImageResponse, SPP -from app.pck_managers.looped import get_looping +from app.definitions.input import SurveyMetadata +from app.definitions.output import SPP +from app.controllers.looped import get_looping from tests.integration.looped import read_submission_data class BerdTests(unittest.TestCase): - def test_to_image(self): - filepath = "tests/data/berd/002.0001.json" - submission_data = read_submission_data(filepath) - - survey_metadata: SurveyMetadata = { - "survey_id": "002", - "period_id": "201605", - "ru_ref": "12346789012A", - "form_type": "0001", - "period_start_date": "2016-05-01", - "period_end_date": "2016-05-31", - } - - actual: list[ImageResponse] = json.loads(get_looping(submission_data, survey_metadata, True)) - - image_filepath = "tests/data/berd/002.0001-image.json" - with open(image_filepath) as f: - expected: list[ImageResponse] = json.load(f) - - expected.sort(key=lambda i: i['instance']) - actual.sort(key=lambda i: i['instance']) - - # print(json.dumps(actual)) - - self.assertEqual(expected, actual) - def test_to_spp(self): filepath = "tests/data/berd/002.0001.json" @@ -48,10 +23,10 @@ def test_to_spp(self): "period_end_date": "2016-05-31", } - actual: SPP = json.loads(get_looping(submission_data, survey_metadata, False)) + actual: SPP = json.loads(get_looping(submission_data, survey_metadata)) spp_filepath = "tests/data/berd/002.0001-spp.json" with open(spp_filepath) as f: - expected: list[ImageResponse] = json.load(f) + expected: SPP = json.load(f) self.assertEqual(expected, actual) diff --git a/tests/integration/berd/test_berd_short.py b/tests/integration/berd/test_berd_short.py index 8b939b6..f5c7b12 100644 --- a/tests/integration/berd/test_berd_short.py +++ b/tests/integration/berd/test_berd_short.py @@ -1,9 +1,10 @@ import json import unittest -from app.definitions import SurveyMetadata, ImageResponse, SPP -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data +from app.definitions.input import SurveyMetadata +from app.definitions.output import SPP +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data class BerdTests(unittest.TestCase): @@ -26,6 +27,6 @@ def test_to_pck(self): spp_filepath = "tests/data/berd/002.0006-spp.json" with open(spp_filepath) as f: - expected: list[ImageResponse] = json.load(f) + expected: SPP = json.load(f) self.assertEqual(expected, actual) diff --git a/tests/integration/mapped/__init__.py b/tests/integration/flat/__init__.py similarity index 74% rename from tests/integration/mapped/__init__.py rename to tests/integration/flat/__init__.py index 7251275..48ab2dc 100644 --- a/tests/integration/mapped/__init__.py +++ b/tests/integration/flat/__init__.py @@ -1,6 +1,18 @@ import json -from app.definitions import Data, Empty, PCK +from app.config.dependencies import get_flat_transformer, get_build_spec_mapping, get_spec_repository, get_executor, \ + get_func_lookup, get_formatter_mapping +from app.definitions.input import Data, Empty, SurveyMetadata +from app.definitions.output import PCK + + +def get_transformer(survey_metadata: SurveyMetadata): + return get_flat_transformer( + survey_metadata, + get_build_spec_mapping(get_spec_repository()), + get_executor(get_func_lookup()), + get_formatter_mapping(), + ) def read_submission_data(filepath: str) -> Data: diff --git a/tests/integration/mapped/test_abs.py b/tests/integration/flat/test_abs.py similarity index 85% rename from tests/integration/mapped/test_abs.py rename to tests/integration/flat/test_abs.py index 5bf8db1..bd13e5a 100644 --- a/tests/integration/mapped/test_abs.py +++ b/tests/integration/flat/test_abs.py @@ -1,9 +1,10 @@ import os import unittest -from app.definitions import SurveyMetadata, PCK, Data -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import Data, SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class ABSPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_acas.py b/tests/integration/flat/test_acas.py similarity index 89% rename from tests/integration/mapped/test_acas.py rename to tests/integration/flat/test_acas.py index 1835309..a4b6592 100644 --- a/tests/integration/mapped/test_acas.py +++ b/tests/integration/flat/test_acas.py @@ -1,8 +1,11 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import transform, get_build_spec, get_pck -from tests.integration.mapped import read_submission_data, remove_empties, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.definitions.spec import ParseTree +from app.controllers.flat import get_pck +from app.transformers.flat import FlatSpecTransformer +from tests.integration.flat import read_submission_data, remove_empties, are_equal, get_transformer class ACASTransformTests(unittest.TestCase): @@ -11,8 +14,18 @@ def test_negatives_and_missing_comment(self): filepath = "tests/data/acas/acas.json" submission_data = read_submission_data(filepath) - build_spec = get_build_spec("171", {"171": "acas"}) - transformed_data = transform(submission_data, build_spec) + transformer: FlatSpecTransformer = get_transformer( + { + "survey_id": "171", + "period_id": "201605", + "ru_ref": "12346789012A", + "form_type": "0002", + "period_start_date": "2016-05-01", + "period_end_date": "2016-05-31", + }, + ) + parse_tree: ParseTree = transformer.interpolate() + transformed_data = transformer.run(parse_tree, submission_data) actual = remove_empties(transformed_data) expected = { diff --git a/tests/integration/mapped/test_blocks.py b/tests/integration/flat/test_blocks.py similarity index 78% rename from tests/integration/mapped/test_blocks.py rename to tests/integration/flat/test_blocks.py index e203005..563c568 100644 --- a/tests/integration/mapped/test_blocks.py +++ b/tests/integration/flat/test_blocks.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class BlocksPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_bricks.py b/tests/integration/flat/test_bricks.py similarity index 74% rename from tests/integration/mapped/test_bricks.py rename to tests/integration/flat/test_bricks.py index f44a3ff..d92f05c 100644 --- a/tests/integration/mapped/test_bricks.py +++ b/tests/integration/flat/test_bricks.py @@ -1,11 +1,33 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_build_spec, transform, get_pck -from tests.integration.mapped import remove_empties, read_submission_data, are_equal +from app.config.dependencies import get_flat_transformer, get_build_spec_mapping, get_spec_repository, get_executor, \ + get_formatter_mapping, get_func_lookup +from app.controllers.flat import get_pck +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.definitions.spec import ParseTree +from tests.integration.flat import remove_empties, read_submission_data, are_equal + +survey_metadata: SurveyMetadata = { + "survey_id": "074", + "period_id": "201605", + "ru_ref": "12346789012A", + "form_type": "0001", + "period_start_date": "2016-05-01", + "period_end_date": "2016-05-31", +} class BricksTransformsTests(unittest.TestCase): + + def setUp(self): + self.transformer = get_flat_transformer( + survey_metadata, + get_build_spec_mapping(get_spec_repository()), + get_executor(get_func_lookup()), + get_formatter_mapping(), + ) + def test_bricks_prepend(self): types = { "Clay": "2", @@ -26,8 +48,6 @@ def test_bricks_prepend(self): "24": "0", "9999": "Concrete"} - build_spec = get_build_spec("074", {"074": "bricks"}) - for k, v in types.items(): submission_data["9999"] = k expected = {f"{v}01": "0", @@ -48,7 +68,10 @@ def test_bricks_prepend(self): "502": "0", "503": "0", "504": "0"} - transformed_data = transform(submission_data, build_spec) + + transformer = self.transformer + parse_tree: ParseTree = transformer.interpolate() + transformed_data = transformer.run(parse_tree, submission_data) actual = remove_empties(transformed_data) actual = actual.keys() expected = expected.keys() @@ -58,15 +81,16 @@ def test_bricks_text_transform(self): submission_data = {"145": "I am a comment that should be replaced with a 1", "146": ""} - build_spec = get_build_spec("074", {"074": "bricks"}) - expected = {"145": "1", "146": "2", "501": "0", "502": "0", "503": "0", "504": "0"} - transformed_data = transform(submission_data, build_spec) + + transformer = self.transformer + parse_tree: ParseTree = transformer.interpolate() + transformed_data = transformer.run(parse_tree, submission_data) actual = remove_empties(transformed_data) self.assertEqual(expected, actual) @@ -90,8 +114,6 @@ def test_bricks_totals_transform(self): "24": "7", "9999": ""} - build_spec = get_build_spec("074", {"074": "bricks"}) - for k, v in types.items(): submission_data["9999"] = k expected = {f"{v}01": "10", @@ -113,24 +135,19 @@ def test_bricks_totals_transform(self): "503": "250", "504": "14" } - transformed_data = transform(submission_data, build_spec) + + transformer = self.transformer + parse_tree: ParseTree = transformer.interpolate() + transformed_data = transformer.run(parse_tree, submission_data) actual = remove_empties(transformed_data) self.assertEqual(expected, actual) class BricksPckTests(unittest.TestCase): + def test_0002_to_pck(self): filepath = "tests/data/bricks/074.0001.json" submission_data = read_submission_data(filepath) - - survey_metadata: SurveyMetadata = { - "survey_id": "074", - "period_id": "201605", - "ru_ref": "12346789012A", - "form_type": "0001", - "period_start_date": "2016-05-01", - "period_end_date": "2016-05-31", - } actual: PCK = get_pck(submission_data, survey_metadata) pck_filepath = "tests/data/bricks/074.0001.pck" with open(pck_filepath) as f: diff --git a/tests/integration/mapped/test_construction.py b/tests/integration/flat/test_construction.py similarity index 86% rename from tests/integration/mapped/test_construction.py rename to tests/integration/flat/test_construction.py index f3bf63d..aa64772 100644 --- a/tests/integration/mapped/test_construction.py +++ b/tests/integration/flat/test_construction.py @@ -1,8 +1,10 @@ +import json import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class ConstructionPckTests(unittest.TestCase): @@ -25,6 +27,26 @@ def test_0001_to_pck(self): self.assertTrue(are_equal(expected, actual)) + def test_0001_to_spp(self): + filepath = "tests/data/construction/228.0001.json" + submission_data = read_submission_data(filepath) + + survey_metadata: SurveyMetadata = { + "survey_id": "228", + "period_id": "2504", + "ru_ref": "48514665167x", + "form_type": "0001", + "period_start_date": "2025-04-01", + "period_end_date": "2025-07-01", + } + actual: PCK = get_pck(submission_data, survey_metadata) + + pck_filepath = "tests/data/construction/228.0001-spp.json" + with open(pck_filepath) as f: + expected: PCK = f.read() + + self.assertTrue(json.loads(expected), json.loads(actual)) + def test_0001_no_comment_to_pck(self): filepath = "tests/data/construction/228.0001.no.comment.json" submission_data = read_submission_data(filepath) diff --git a/tests/integration/mapped/test_des.py b/tests/integration/flat/test_des.py similarity index 87% rename from tests/integration/mapped/test_des.py rename to tests/integration/flat/test_des.py index 5c94eca..ef71bc6 100644 --- a/tests/integration/mapped/test_des.py +++ b/tests/integration/flat/test_des.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class DesPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_fuels.py b/tests/integration/flat/test_fuels.py similarity index 84% rename from tests/integration/mapped/test_fuels.py rename to tests/integration/flat/test_fuels.py index 72fcd26..37825fe 100644 --- a/tests/integration/mapped/test_fuels.py +++ b/tests/integration/flat/test_fuels.py @@ -1,9 +1,9 @@ import json import unittest -from app.definitions import SurveyMetadata -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data +from app.definitions.input import SurveyMetadata +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data class FuelsTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_mbs.py b/tests/integration/flat/test_mbs.py similarity index 93% rename from tests/integration/mapped/test_mbs.py rename to tests/integration/flat/test_mbs.py index e6dd6af..71c9dd9 100644 --- a/tests/integration/mapped/test_mbs.py +++ b/tests/integration/flat/test_mbs.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class MBSPckTests(unittest.TestCase): diff --git a/tests/integration/flat/test_mbs_spp.py b/tests/integration/flat/test_mbs_spp.py new file mode 100644 index 0000000..b9b4466 --- /dev/null +++ b/tests/integration/flat/test_mbs_spp.py @@ -0,0 +1,43 @@ +import json +import unittest + +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data + + +class MbsSppTests(unittest.TestCase): + + def _run_test(self, form_type: str, period_id: str): + filepath = f"tests/data/mbs/009.{form_type}.json" + submission_data = read_submission_data(filepath) + + survey_metadata: SurveyMetadata = { + "survey_id": "009", + "period_id": period_id, + "ru_ref": "97148856319Y", + "form_type": form_type, + "period_start_date": "2025-03-01", + "period_end_date": "2025-03-31", + } + + actual: PCK = get_pck(submission_data, survey_metadata) + + pck_filepath = f"tests/data/mbs/009.{form_type}-spp.json" + with open(pck_filepath) as f: + expected: PCK = f.read() + + self.assertEqual(json.loads(expected), json.loads(actual)) + + def test_0106_to_pck(self): + self._run_test(form_type="0106", period_id="2503") + + def test_0111_to_pck(self): + self._run_test(form_type="0111", period_id="2503") + + def test_0255_to_pck(self): + self._run_test(form_type="0255", period_id="2503") + + def test_0867_to_pck(self): + self._run_test(form_type="0867", period_id="2503") diff --git a/tests/integration/mapped/test_mcg.py b/tests/integration/flat/test_mcg.py similarity index 91% rename from tests/integration/mapped/test_mcg.py rename to tests/integration/flat/test_mcg.py index 000db0e..e358e0c 100644 --- a/tests/integration/mapped/test_mcg.py +++ b/tests/integration/flat/test_mcg.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class MCGPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_mes.py b/tests/integration/flat/test_mes.py similarity index 91% rename from tests/integration/mapped/test_mes.py rename to tests/integration/flat/test_mes.py index 20c5aee..02981d6 100644 --- a/tests/integration/mapped/test_mes.py +++ b/tests/integration/flat/test_mes.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class UKISPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_mwss.py b/tests/integration/flat/test_mwss.py similarity index 61% rename from tests/integration/mapped/test_mwss.py rename to tests/integration/flat/test_mwss.py index ee5373d..c25b254 100644 --- a/tests/integration/mapped/test_mwss.py +++ b/tests/integration/flat/test_mwss.py @@ -1,8 +1,20 @@ import unittest -from app.definitions import PCK, SurveyMetadata -from app.pck_managers.mapped import get_pck, transform, get_build_spec -from tests.integration.mapped import read_submission_data, remove_empties, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.definitions.spec import ParseTree +from app.controllers.flat import get_pck +from app.transformers.flat import FlatSpecTransformer +from tests.integration.flat import read_submission_data, remove_empties, are_equal, get_transformer + +survey_metadata: SurveyMetadata = { + "survey_id": "134", + "period_id": "201605", + "ru_ref": "12346789012A", + "form_type": "0005", + "period_start_date": "2016-05-01", + "period_end_date": "2016-05-31", +} class MWSSTransformTests(unittest.TestCase): @@ -11,8 +23,9 @@ def test_mwss_minimal(self): filepath = "tests/data/mwss/mwss_minimal.json" submission_data = read_submission_data(filepath) - build_spec = get_build_spec("134", {"134": "mwss"}) - transformed_data = transform(submission_data, build_spec) + transformer: FlatSpecTransformer = get_transformer(survey_metadata) + parse_tree: ParseTree = transformer.interpolate() + transformed_data = transformer.run(parse_tree, submission_data) actual = remove_empties(transformed_data) expected = {'40': '1', '50': '100', '60': '10', '70': '20', '80': '30', '90': '2', '130': '2', '131': '2', @@ -23,8 +36,9 @@ def test_mwss_full(self): filepath = "tests/data/mwss/mwss_full.json" submission_data = read_submission_data(filepath) - build_spec = get_build_spec("134", {"134": "mwss"}) - transformed_data = transform(submission_data, build_spec) + transformer: FlatSpecTransformer = get_transformer(survey_metadata) + parse_tree: ParseTree = transformer.interpolate() + transformed_data = transformer.run(parse_tree, submission_data) actual = remove_empties(transformed_data) expected = {'40': '30', '50': '49450', '60': '1300', '70': '1050', '80': '1600', '90': '1', '100': '1', @@ -40,15 +54,6 @@ def test_134_to_pck(self): filepath = "tests/data/mwss/134.0005.json" submission_data = read_submission_data(filepath) - survey_metadata: SurveyMetadata = { - "survey_id": "134", - "period_id": "201605", - "ru_ref": "12346789012A", - "form_type": "0005", - "period_start_date": "2016-05-01", - "period_end_date": "2016-05-31", - } - actual: PCK = get_pck(submission_data, survey_metadata) pck_filepath = "tests/data/mwss/134.0005.pck" diff --git a/tests/integration/mapped/test_qbs.py b/tests/integration/flat/test_qbs.py similarity index 70% rename from tests/integration/mapped/test_qbs.py rename to tests/integration/flat/test_qbs.py index 40af88e..b50e8c0 100644 --- a/tests/integration/mapped/test_qbs.py +++ b/tests/integration/flat/test_qbs.py @@ -1,8 +1,10 @@ +import json import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class QBSTest(unittest.TestCase): @@ -28,6 +30,27 @@ def test_0001_to_pck(self): self.assertTrue(are_equal(expected, actual)) + def test_0001_to_spp(self): + filepath = "tests/data/qbs/139.0001.json" + submission_data = read_submission_data(filepath) + + survey_metadata: SurveyMetadata = { + "survey_id": "139", + "period_id": "2503", + "ru_ref": "12345678901A", + "form_type": "0001", + "period_start_date": "2025-03-01", + "period_end_date": "2025-06-01", + } + + actual: PCK = get_pck(submission_data, survey_metadata) + + pck_filepath = "tests/data/qbs/139.0001-spp.json" + with open(pck_filepath) as f: + expected: PCK = f.read() + + self.assertTrue(json.loads(expected), json.loads(actual)) + def test_0001_missing_total_to_pck(self): filepath = "tests/data/qbs/139.0001.missing.total.json" submission_data = read_submission_data(filepath) diff --git a/tests/integration/mapped/test_qcas.py b/tests/integration/flat/test_qcas.py similarity index 84% rename from tests/integration/mapped/test_qcas.py rename to tests/integration/flat/test_qcas.py index 9bde9e7..7b5e604 100644 --- a/tests/integration/mapped/test_qcas.py +++ b/tests/integration/flat/test_qcas.py @@ -1,9 +1,10 @@ import os import unittest -from app.definitions import SurveyMetadata, PCK, Data -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import Data, SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class QCASPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_qpses.py b/tests/integration/flat/test_qpses.py similarity index 78% rename from tests/integration/mapped/test_qpses.py rename to tests/integration/flat/test_qpses.py index 9eea5fd..70a31a9 100644 --- a/tests/integration/mapped/test_qpses.py +++ b/tests/integration/flat/test_qpses.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class QPSESPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_qpsespb.py b/tests/integration/flat/test_qpsespb.py similarity index 79% rename from tests/integration/mapped/test_qpsespb.py rename to tests/integration/flat/test_qpsespb.py index 9fb8ef7..ea53f40 100644 --- a/tests/integration/mapped/test_qpsespb.py +++ b/tests/integration/flat/test_qpsespb.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class QPSESPBPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_qpsesrap.py b/tests/integration/flat/test_qpsesrap.py similarity index 79% rename from tests/integration/mapped/test_qpsesrap.py rename to tests/integration/flat/test_qpsesrap.py index 53e7b01..8cfbcfa 100644 --- a/tests/integration/mapped/test_qpsesrap.py +++ b/tests/integration/flat/test_qpsesrap.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class QPSESRAPPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_rails.py b/tests/integration/flat/test_rails.py similarity index 84% rename from tests/integration/mapped/test_rails.py rename to tests/integration/flat/test_rails.py index 63b4f95..add86c8 100644 --- a/tests/integration/mapped/test_rails.py +++ b/tests/integration/flat/test_rails.py @@ -1,9 +1,9 @@ import json import unittest -from app.definitions import SurveyMetadata -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data +from app.definitions.input import SurveyMetadata +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data class RailsTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_stocks.py b/tests/integration/flat/test_stocks.py similarity index 84% rename from tests/integration/mapped/test_stocks.py rename to tests/integration/flat/test_stocks.py index ccfac77..51921f1 100644 --- a/tests/integration/mapped/test_stocks.py +++ b/tests/integration/flat/test_stocks.py @@ -1,9 +1,10 @@ import os import unittest -from app.definitions import SurveyMetadata, PCK, Data -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import Data, SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class StocksPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_ukis.py b/tests/integration/flat/test_ukis.py similarity index 87% rename from tests/integration/mapped/test_ukis.py rename to tests/integration/flat/test_ukis.py index e4b6951..a102b62 100644 --- a/tests/integration/mapped/test_ukis.py +++ b/tests/integration/flat/test_ukis.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class UKISPckTests(unittest.TestCase): diff --git a/tests/integration/mapped/test_vacancies.py b/tests/integration/flat/test_vacancies.py similarity index 85% rename from tests/integration/mapped/test_vacancies.py rename to tests/integration/flat/test_vacancies.py index 0b4f833..1d67646 100644 --- a/tests/integration/mapped/test_vacancies.py +++ b/tests/integration/flat/test_vacancies.py @@ -1,9 +1,10 @@ import os import unittest -from app.definitions import SurveyMetadata, PCK, Data -from app.pck_managers.mapped import get_pck -from tests.integration.mapped import read_submission_data, are_equal +from app.definitions.input import Data, SurveyMetadata +from app.definitions.output import PCK +from app.controllers.flat import get_pck +from tests.integration.flat import read_submission_data, are_equal class VacanciesTest(unittest.TestCase): diff --git a/tests/integration/looped/__init__.py b/tests/integration/looped/__init__.py index f15b5f2..48be5ce 100644 --- a/tests/integration/looped/__init__.py +++ b/tests/integration/looped/__init__.py @@ -1,6 +1,6 @@ import json -from app.definitions import ListCollector, Data +from app.definitions.input import Data, ListCollector def read_submission_data(filepath: str) -> ListCollector: diff --git a/tests/integration/looped/test_bres.py b/tests/integration/looped/test_bres.py index df6d593..13d2885 100644 --- a/tests/integration/looped/test_bres.py +++ b/tests/integration/looped/test_bres.py @@ -1,7 +1,8 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.pck_managers.looped import get_looping +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.looped import get_looping from tests.integration.looped import read_submission_data diff --git a/tests/integration/looped/test_looping.py b/tests/integration/looped/test_looping.py index a0f1f0e..956e069 100644 --- a/tests/integration/looped/test_looping.py +++ b/tests/integration/looped/test_looping.py @@ -1,10 +1,11 @@ import json import unittest -from app.definitions import SurveyMetadata, PCK, LoopedData, SPP -from app.pck_managers.looped import convert_to_looped_data, get_looping +from app.definitions.input import SurveyMetadata, LoopedData +from app.definitions.output import SPP, PCK +from app.controllers.looped import convert_to_looped_data, get_looping from tests.integration.looped import read_submission_data -from tests.integration.mapped import are_equal +from tests.integration.flat import are_equal class LoopingTests(unittest.TestCase): @@ -51,7 +52,7 @@ def test_convert_to_looped_data(self): } - self.assertEquals(expected, actual) + self.assertEqual(expected, actual) def test_looped_to_cora_pck(self): diff --git a/tests/integration/looped/test_qrt.py b/tests/integration/looped/test_qrt.py deleted file mode 100644 index ee134aa..0000000 --- a/tests/integration/looped/test_qrt.py +++ /dev/null @@ -1,34 +0,0 @@ -import json -import unittest - -from app.definitions import SurveyMetadata, ImageResponse -from app.pck_managers.looped import get_looping -from tests.integration.looped import read_submission_data - - -class QrtTests(unittest.TestCase): - - def test_to_image(self): - - filepath = "tests/data/tiles/068.0001.json" - submission_data = read_submission_data(filepath) - - survey_metadata: SurveyMetadata = { - "survey_id": "068", - "period_id": "201605", - "ru_ref": "12346789012A", - "form_type": "0001", - "period_start_date": "2016-05-01", - "period_end_date": "2016-05-31", - } - - actual: list[ImageResponse] = json.loads(get_looping(submission_data, survey_metadata, use_image_formatter=True)) - - spp_filepath = "tests/data/tiles/068-image.json" - with open(spp_filepath) as f: - expected: list[ImageResponse] = json.load(f) - - expected.sort(key=lambda i: i['instance']) - actual.sort(key=lambda i: i['instance']) - - self.assertEqual(expected, actual) diff --git a/tests/integration/looped/test_qs.py b/tests/integration/looped/test_qs.py deleted file mode 100644 index ec1eff1..0000000 --- a/tests/integration/looped/test_qs.py +++ /dev/null @@ -1,34 +0,0 @@ -import json -import unittest - -from app.definitions import SurveyMetadata, ImageResponse -from app.pck_managers.looped import get_looping -from tests.integration.looped import read_submission_data - - -class QsTests(unittest.TestCase): - - def test_to_image(self): - - filepath = "tests/data/slate/071.0001.json" - submission_data = read_submission_data(filepath) - - survey_metadata: SurveyMetadata = { - "survey_id": "071", - "period_id": "201605", - "ru_ref": "12346789012A", - "form_type": "0001", - "period_start_date": "2016-05-01", - "period_end_date": "2016-05-31", - } - - actual: list[ImageResponse] = json.loads(get_looping(submission_data, survey_metadata, use_image_formatter=True)) - - spp_filepath = "tests/data/slate/071-image.json" - with open(spp_filepath) as f: - expected: list[ImageResponse] = json.load(f) - - expected.sort(key=lambda i: i['instance']) - actual.sort(key=lambda i: i['instance']) - - self.assertEqual(expected, actual) diff --git a/tests/integration/looped/test_qsl.py b/tests/integration/looped/test_qsl.py index 9205fb1..f7bba1c 100644 --- a/tests/integration/looped/test_qsl.py +++ b/tests/integration/looped/test_qsl.py @@ -1,45 +1,14 @@ -import json import unittest -from app.definitions import SurveyMetadata, ImageResponse, PCK -from app.pck_managers.looped import get_looping +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.looped import get_looping from tests.integration.looped import read_submission_data -from tests.integration.mapped import are_equal +from tests.integration.flat import are_equal class QslTests(unittest.TestCase): - def test_to_image(self): - - filepath = "tests/data/land/066.0002.json" - submission_data = read_submission_data(filepath) - - survey_metadata: SurveyMetadata = { - "survey_id": "066", - "period_id": "201605", - "ru_ref": "12346789012A", - "form_type": "0002", - "period_start_date": "2016-05-01", - "period_end_date": "2016-05-31", - } - - actual: list[ImageResponse] = json.loads(get_looping( - submission_data, - survey_metadata, - use_image_formatter=True)) - - print("") - print(actual) - - image_filepath = "tests/data/land/066.0002-image.json" - with open(image_filepath) as f: - expected: list[ImageResponse] = json.load(f) - - expected.sort(key=lambda i: i['instance']) - actual.sort(key=lambda i: i['instance']) - - self.assertEqual(expected, actual) - def test_to_cs_pck(self): filepath = "tests/data/land/066.0002.json" diff --git a/tests/integration/looped/test_qsm.py b/tests/integration/looped/test_qsm.py index ff09b7a..dc0d60f 100644 --- a/tests/integration/looped/test_qsm.py +++ b/tests/integration/looped/test_qsm.py @@ -1,42 +1,14 @@ -import json import unittest -from app.definitions import SurveyMetadata, ImageResponse, PCK -from app.pck_managers.looped import get_looping +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.controllers.looped import get_looping from tests.integration.looped import read_submission_data -from tests.integration.mapped import are_equal +from tests.integration.flat import are_equal class QsmTests(unittest.TestCase): - def test_to_image(self): - - filepath = "tests/data/marine/076.0002.json" - submission_data = read_submission_data(filepath) - - survey_metadata: SurveyMetadata = { - "survey_id": "076", - "period_id": "201605", - "ru_ref": "12346789012A", - "form_type": "0002", - "period_start_date": "2016-05-01", - "period_end_date": "2016-05-31", - } - - actual: list[ImageResponse] = json.loads(get_looping( - submission_data, - survey_metadata, - use_image_formatter=True)) - - image_filepath = "tests/data/marine/076.0002-image.json" - with open(image_filepath) as f: - expected: list[ImageResponse] = json.load(f) - - expected.sort(key=lambda i: i['instance']) - actual.sort(key=lambda i: i['instance']) - - self.assertEqual(expected, actual) - def test_to_cs_pck(self): filepath = "tests/data/marine/076.0002.json" diff --git a/tests/integration/prepop/test_bres.py b/tests/integration/prepop/test_bres.py index 5e6c1ff..d5f66e2 100644 --- a/tests/integration/prepop/test_bres.py +++ b/tests/integration/prepop/test_bres.py @@ -1,8 +1,9 @@ import json import unittest -from app.definitions import PrepopData, Identifier, Template -from app.pck_managers.prepop import get_prepop +from app.definitions.spec import Template +from app.definitions.input import Identifier, PrepopData +from app.controllers.prepop import get_prepop class BresTest(unittest.TestCase): diff --git a/tests/integration/prepop/test_brs.py b/tests/integration/prepop/test_brs.py index 13cdd96..19e4be7 100644 --- a/tests/integration/prepop/test_brs.py +++ b/tests/integration/prepop/test_brs.py @@ -1,8 +1,9 @@ import json import unittest -from app.definitions import PrepopData, Identifier, Template -from app.pck_managers.prepop import get_prepop +from app.definitions.spec import Template +from app.definitions.input import Identifier, PrepopData +from app.controllers.prepop import get_prepop class BrsTest(unittest.TestCase): diff --git a/tests/integration/prepop/test_land.py b/tests/integration/prepop/test_land.py index 49c3534..184e973 100644 --- a/tests/integration/prepop/test_land.py +++ b/tests/integration/prepop/test_land.py @@ -1,8 +1,9 @@ import json import unittest -from app.definitions import PrepopData, Identifier, Template -from app.pck_managers.prepop import get_prepop +from app.definitions.spec import Template +from app.definitions.input import Identifier, PrepopData +from app.controllers.prepop import get_prepop class LandTests(unittest.TestCase): diff --git a/tests/integration/prepop/test_marine.py b/tests/integration/prepop/test_marine.py index fb29838..79c8dc8 100644 --- a/tests/integration/prepop/test_marine.py +++ b/tests/integration/prepop/test_marine.py @@ -1,8 +1,9 @@ import json import unittest -from app.definitions import PrepopData, Identifier, Template -from app.pck_managers.prepop import get_prepop +from app.definitions.spec import Template +from app.definitions.input import Identifier, PrepopData +from app.controllers.prepop import get_prepop class MarineTests(unittest.TestCase): diff --git a/tests/integration/prepop/test_slate.py b/tests/integration/prepop/test_slate.py index 09edfc8..aca9494 100644 --- a/tests/integration/prepop/test_slate.py +++ b/tests/integration/prepop/test_slate.py @@ -1,8 +1,9 @@ import json import unittest -from app.definitions import PrepopData, Identifier, Template -from app.pck_managers.prepop import get_prepop +from app.definitions.spec import Template +from app.definitions.input import Identifier, PrepopData +from app.controllers.prepop import get_prepop class SlateTests(unittest.TestCase): diff --git a/tests/integration/prepop/test_tiles.py b/tests/integration/prepop/test_tiles.py index 7e078c9..a8854fb 100644 --- a/tests/integration/prepop/test_tiles.py +++ b/tests/integration/prepop/test_tiles.py @@ -1,8 +1,9 @@ import json import unittest -from app.definitions import PrepopData, Identifier, Template -from app.pck_managers.prepop import get_prepop +from app.definitions.spec import Template +from app.definitions.input import Identifier, PrepopData +from app.controllers.prepop import get_prepop class TilesTests(unittest.TestCase): diff --git a/tests/unit/berd/test_berd_transformer.py b/tests/unit/berd/test_berd_transformer.py index b84a168..e59a1e5 100644 --- a/tests/unit/berd/test_berd_transformer.py +++ b/tests/unit/berd/test_berd_transformer.py @@ -1,11 +1,11 @@ import json import unittest -from app.berd.collect_items import collect_list_items -from app.berd.convert_data import extract_answers, convert_to_spp -from app.berd.definitions import Answer, SPP -from app.definitions import SurveyMetadata -from app.pck_managers.looped import get_looping +from app.services.berd.collect_items import collect_list_items +from app.services.berd.convert_data import extract_answers, convert_to_spp +from app.services.berd.definitions import Answer, SPP +from app.definitions.input import SurveyMetadata +from app.controllers.looped import get_looping class BERDTransformerTests(unittest.TestCase): @@ -59,7 +59,7 @@ def test_full_transform(self): "period_end_date": "2016-05-31", } - actual = get_looping(data, survey_metadata, False) + actual = get_looping(data, survey_metadata) expected = { 'formtype': '0001', diff --git a/tests/unit/berd/test_collect_items.py b/tests/unit/berd/test_collect_items.py index 29e1007..4b0bd16 100644 --- a/tests/unit/berd/test_collect_items.py +++ b/tests/unit/berd/test_collect_items.py @@ -1,7 +1,7 @@ import unittest -from app.berd.collect_items import is_subset_of, collect_list_items -from app.berd.definitions import Answer +from app.services.berd.collect_items import is_subset_of, collect_list_items +from app.services.berd.definitions import Answer class CollectItemsTests(unittest.TestCase): diff --git a/tests/unit/berd/test_convert_data.py b/tests/unit/berd/test_convert_data.py index 633a03a..5ebefbc 100644 --- a/tests/unit/berd/test_convert_data.py +++ b/tests/unit/berd/test_convert_data.py @@ -1,8 +1,8 @@ import unittest -from app.berd.convert_data import spp_from_map, extract_answers, convert_to_spp, remove_prepend_values, \ +from app.services.berd.convert_data import spp_from_map, extract_answers, convert_to_spp, remove_prepend_values, \ convert_civil_defence -from app.berd.definitions import SPP, Answer +from app.services.berd.definitions import SPP, Answer class SppFromMapTest(unittest.TestCase): diff --git a/tests/unit/formatters/test_cora_looping_formatter.py b/tests/unit/formatters/test_cora_looping_formatter.py index bec6d0a..b9b4e08 100644 --- a/tests/unit/formatters/test_cora_looping_formatter.py +++ b/tests/unit/formatters/test_cora_looping_formatter.py @@ -1,8 +1,9 @@ import unittest -from app.definitions import SurveyMetadata, PCK -from app.formatters.cora_looping_formatter import CORALoopingFormatter -from tests.integration.mapped import are_equal +from app.definitions.input import SurveyMetadata +from app.definitions.output import PCK +from app.services.formatters.cora_looping_formatter import CORALoopingFormatter +from tests.integration.flat import are_equal class CoraLoopingFormatterTest(unittest.TestCase): diff --git a/tests/unit/formatters/test_formatter.py b/tests/unit/formatters/test_formatter.py index 987318f..f5976bf 100644 --- a/tests/unit/formatters/test_formatter.py +++ b/tests/unit/formatters/test_formatter.py @@ -1,6 +1,6 @@ import unittest -from app.formatters.formatter import Formatter +from app.services.formatters.formatter import Formatter class ConvertPeriodTests(unittest.TestCase): diff --git a/tests/unit/period/__init__.py b/tests/unit/period/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/period/test_period.py b/tests/unit/period/test_period.py new file mode 100644 index 0000000..16d4fa3 --- /dev/null +++ b/tests/unit/period/test_period.py @@ -0,0 +1,217 @@ +import unittest + +from app.services.period.period import Period, PeriodFormatError + + +class ConvertPeriodTests(unittest.TestCase): + + def test_DDMMYY_to_DDMMYY(self): + period = Period("310723", "DDMMYY") + actual = period.convert_to_format("DDMMYY") + expected = "310723" + self.assertEqual(expected, actual) + + def test_DDMMYY_to_MMYY(self): + period = Period("310723", "DDMMYY") + actual = period.convert_to_format("MMYY") + expected = "0723" + self.assertEqual(expected, actual) + + def test_DDMMYY_to_YYMMDD(self): + period = Period("310723", "DDMMYY") + actual = period.convert_to_format("YYMMDD") + expected = "230731" + self.assertEqual(expected, actual) + + def test_DDMMYY_to_YYYY(self): + period = Period("310723", "DDMMYY") + actual = period.convert_to_format("YYYY") + expected = "2023" + self.assertEqual(expected, actual) + + def test_MMYYYY_to_YYYY(self): + period = Period("072023", "MMYYYY") + actual = period.convert_to_format("YYYY") + expected = "2023" + self.assertEqual(expected, actual) + + def test_YYYYMM_to_YYYYMM(self): + period = Period("202307", "YYYYMM") + actual = period.convert_to_format("YYYYMM") + expected = "202307" + self.assertEqual(expected, actual) + + def test_YYYY_to_YY(self): + period = Period("2023", "YYYY") + actual = period.convert_to_format("YY") + expected = "23" + self.assertEqual(expected, actual) + + def test_YYYYMMDD_to_MMYYYY(self): + period = Period("20230731", "YYYYMMDD") + actual = period.convert_to_format("MMYYYY") + expected = "072023" + self.assertEqual(expected, actual) + + def test_incorrect_format_uses_default_value(self): + period = Period("202310", "YY") + actual = period.convert_to_format("YY") + expected = "23" + self.assertEqual(expected, actual) + + def test_incorrect_format_uses_default_value_length_4(self): + period = Period("2301", "YY") + actual = period.convert_to_format("YY") + expected = "23" + self.assertEqual(expected, actual) + + def test_incorrect_format_uses_default_value_length_4_with_different_pck_format(self): + period = Period("2301", "YYYYMM") + actual = period.convert_to_format("YYYYMM") + expected = "202301" + self.assertEqual(expected, actual) + + def test_incorrect_format_uses_default_value_length_2(self): + period = Period("23", "YYYYMM") + actual = period.convert_to_format("YYYYMM") + expected = "202301" + self.assertEqual(expected, actual) + + +class ComparePeriodTests(unittest.TestCase): + + def test_gt_YYYYMM(self): + period1 = Period("202503", "YYYYMM") + period2 = Period("202502", "YYYYMM") + self.assertTrue(period1 > period2) + + def test_gt_DDMMYY(self): + period1 = Period("010124", "DDMMYY") + period2 = Period("311223", "DDMMYY") + self.assertTrue(period1 > period2) + + def test_gt_YYMM(self): + period1 = Period("2401", "YYMM") + period2 = Period("2312", "YYMM") + self.assertTrue(period1 > period2) + + def test_gt_MMYYYY(self): + period1 = Period("012024", "MMYYYY") + period2 = Period("122023", "MMYYYY") + self.assertTrue(period1 > period2) + + def test_gt_YYYYMMDD(self): + period1 = Period("20240101", "YYYYMMDD") + period2 = Period("20231231", "YYYYMMDD") + self.assertTrue(period1 > period2) + + def test_gt_YYYYMM_YYMM(self): + period1 = Period("202503", "YYYYMM") + period2 = Period("2502", "YYMM") + self.assertTrue(period1 > period2) + + def test_gt_YYYY_YY(self): + period1 = Period("2025", "YYYY") + period2 = Period("24", "YY") + self.assertTrue(period1 > period2) + + def test_gt_incompatible(self): + period1 = Period("2025", "YYYY") + period2 = Period("2504", "YYMM") + with self.assertRaises(PeriodFormatError): + _ = period1 > period2 + + def test_ge_YYYYMM(self): + period1 = Period("202503", "YYYYMM") + period2 = Period("202502", "YYYYMM") + period3 = Period("202503", "YYYYMM") + self.assertTrue(period1 >= period2) + self.assertTrue(period1 >= period3) + + def test_ge_DDMMYY(self): + period1 = Period("010124", "DDMMYY") + period2 = Period("311223", "DDMMYY") + period3 = Period("010124", "DDMMYY") + self.assertTrue(period1 >= period2) + self.assertTrue(period1 >= period3) + + def test_ge_YYMM(self): + period1 = Period("2401", "YYMM") + period2 = Period("2312", "YYMM") + period3 = Period("2401", "YYMM") + self.assertTrue(period1 >= period2) + self.assertTrue(period1 >= period3) + + def test_ge_MMYYYY(self): + period1 = Period("012024", "MMYYYY") + period2 = Period("122023", "MMYYYY") + period3 = Period("012024", "MMYYYY") + self.assertTrue(period1 >= period2) + self.assertTrue(period1 >= period3) + + def test_ge_YYYYMMDD(self): + period1 = Period("20240101", "YYYYMMDD") + period2 = Period("20231231", "YYYYMMDD") + period3 = Period("20240101", "YYYYMMDD") + self.assertTrue(period1 >= period2) + self.assertTrue(period1 >= period3) + + def test_le_YYYYMM(self): + period1 = Period("202502", "YYYYMM") + period2 = Period("202503", "YYYYMM") + period3 = Period("202502", "YYYYMM") + self.assertTrue(period1 <= period2) + self.assertTrue(period1 <= period3) + + def test_le_DDMMYY(self): + period1 = Period("311223", "DDMMYY") + period2 = Period("010124", "DDMMYY") + period3 = Period("311223", "DDMMYY") + self.assertTrue(period1 <= period2) + self.assertTrue(period1 <= period3) + + def test_le_YYMM(self): + period1 = Period("2312", "YYMM") + period2 = Period("2401", "YYMM") + period3 = Period("2312", "YYMM") + self.assertTrue(period1 <= period2) + self.assertTrue(period1 <= period3) + + def test_le_MMYYYY(self): + period1 = Period("122023", "MMYYYY") + period2 = Period("012024", "MMYYYY") + period3 = Period("122023", "MMYYYY") + self.assertTrue(period1 <= period2) + self.assertTrue(period1 <= period3) + + def test_le_YYYYMMDD(self): + period1 = Period("20231231", "YYYYMMDD") + period2 = Period("20240101", "YYYYMMDD") + period3 = Period("20231231", "YYYYMMDD") + self.assertTrue(period1 <= period2) + self.assertTrue(period1 <= period3) + + def test_lt_YYYYMM(self): + period1 = Period("202502", "YYYYMM") + period2 = Period("202503", "YYYYMM") + self.assertTrue(period1 < period2) + + def test_lt_DDMMYY(self): + period1 = Period("311223", "DDMMYY") + period2 = Period("010124", "DDMMYY") + self.assertTrue(period1 < period2) + + def test_lt_YYMM(self): + period1 = Period("2312", "YYMM") + period2 = Period("2401", "YYMM") + self.assertTrue(period1 < period2) + + def test_lt_MMYYYY(self): + period1 = Period("122023", "MMYYYY") + period2 = Period("012024", "MMYYYY") + self.assertTrue(period1 < period2) + + def test_lt_YYYYMMDD(self): + period1 = Period("20231231", "YYYYMMDD") + period2 = Period("20240101", "YYYYMMDD") + self.assertTrue(period1 < period2) diff --git a/tests/unit/transform/functions/test_compound.py b/tests/unit/transform/functions/test_compound.py index 659d62d..5f6dbbb 100644 --- a/tests/unit/transform/functions/test_compound.py +++ b/tests/unit/transform/functions/test_compound.py @@ -1,7 +1,7 @@ import unittest -from app.definitions import Empty -from app.transform.functions.compound import currency_thousands +from app.definitions.input import Empty +from app.services.transform.functions.compound import currency_thousands class CurrencyTests(unittest.TestCase): diff --git a/tests/unit/transform/functions/test_general.py b/tests/unit/transform/functions/test_general.py index 2cbb6f9..0ee3376 100644 --- a/tests/unit/transform/functions/test_general.py +++ b/tests/unit/transform/functions/test_general.py @@ -1,7 +1,7 @@ import unittest -from app.definitions import Empty -from app.transform.functions.general import exists, any_exists +from app.definitions.input import Empty +from app.services.transform.functions.general import exists, any_exists class ExistsTests(unittest.TestCase): diff --git a/tests/unit/transform/functions/test_list.py b/tests/unit/transform/functions/test_list.py index e9d187b..c85a2c3 100644 --- a/tests/unit/transform/functions/test_list.py +++ b/tests/unit/transform/functions/test_list.py @@ -1,7 +1,7 @@ import unittest -from app.definitions import Empty -from app.transform.functions.lists import as_list, append_to_list, prepend_to_list, trim_list +from app.definitions.input import Empty +from app.services.transform.functions.lists import as_list, append_to_list, prepend_to_list, trim_list class ListTests(unittest.TestCase): diff --git a/tests/unit/transform/functions/test_numerical.py b/tests/unit/transform/functions/test_numerical.py index 0452dd9..8ce7e1d 100644 --- a/tests/unit/transform/functions/test_numerical.py +++ b/tests/unit/transform/functions/test_numerical.py @@ -1,7 +1,7 @@ import unittest -from app.definitions import Empty -from app.transform.functions.numerical import round_half_up, aggregate, mean, number_equals, total +from app.definitions.input import Empty +from app.services.transform.functions.numerical import round_half_up, aggregate, mean, number_equals, total class RoundTests(unittest.TestCase): diff --git a/tests/unit/transform/functions/test_string.py b/tests/unit/transform/functions/test_string.py index 21fdcb4..055707c 100644 --- a/tests/unit/transform/functions/test_string.py +++ b/tests/unit/transform/functions/test_string.py @@ -1,7 +1,8 @@ import unittest -from app.definitions import Empty -from app.transform.functions.string import contains, any_contains, concat, starts_with, carve, space_split, postcode +from app.definitions.input import Empty +from app.services.transform.functions.string import (contains, any_contains, concat, + starts_with, carve, space_split, postcode) class StartsWithTests(unittest.TestCase): diff --git a/tests/unit/transform/functions/test_time.py b/tests/unit/transform/functions/test_time.py index a3f0bd8..975f2aa 100644 --- a/tests/unit/transform/functions/test_time.py +++ b/tests/unit/transform/functions/test_time.py @@ -1,7 +1,7 @@ import unittest -from app.definitions import Empty -from app.transform.functions.time import any_date, to_date, start_of_month, end_of_month +from app.definitions.input import Empty +from app.services.transform.functions.time import any_date, to_date, start_of_month, end_of_month class ToDateTests(unittest.TestCase): diff --git a/tests/unit/transform/test_clean.py b/tests/unit/transform/test_clean.py index e54e983..cc5e563 100644 --- a/tests/unit/transform/test_clean.py +++ b/tests/unit/transform/test_clean.py @@ -1,7 +1,7 @@ import unittest -from app.definitions import Template -from app.transform.clean import clean +from app.definitions.spec import Template +from app.services.transform.clean import clean class CleanTests(unittest.TestCase): diff --git a/tests/unit/transform/test_execute.py b/tests/unit/transform/test_execute.py index 6db7987..8786695 100644 --- a/tests/unit/transform/test_execute.py +++ b/tests/unit/transform/test_execute.py @@ -1,9 +1,8 @@ import unittest from collections.abc import Callable -import app.transform.execute -from app.definitions import ParseTree -from app.transform.execute import execute +from app.definitions.spec import ParseTree +from app.services.transform.execute import Executor def fake_remove_chars(value: str, n: str = "1") -> str: @@ -28,7 +27,7 @@ def setUp(self) -> None: "ADD": fake_add, "CONTAINS": fake_contains } - app.transform.execute._function_lookup = fake_function_lookup + self.executor = Executor(fake_function_lookup) def test_execute_single(self): @@ -46,7 +45,7 @@ def test_execute_single(self): "150": "llo", } - actual = execute(parse_tree) + actual = self.executor.execute(parse_tree) self.assertEqual(expected, actual) def test_execute_nested(self): @@ -104,7 +103,7 @@ def test_execute_nested(self): "154": "18", "156": "1000", } - actual = execute(parse_tree) + actual = self.executor.execute(parse_tree) self.assertEqual(expected, actual) def test_execute_transform_in_args(self): @@ -132,7 +131,7 @@ def test_execute_transform_in_args(self): expected = { "152": "1000" } - actual = execute(parse_tree) + actual = self.executor.execute(parse_tree) self.assertEqual(expected, actual) def test_execute_derived(self): @@ -153,7 +152,7 @@ def test_execute_derived(self): "151": "llo" } - actual = execute(parse_tree) + actual = self.executor.execute(parse_tree) self.assertEqual(expected, actual) def test_execute_nested_derived(self): @@ -188,7 +187,7 @@ def test_execute_nested_derived(self): "152": "5" } - actual = execute(parse_tree) + actual = self.executor.execute(parse_tree) self.assertEqual(expected, actual) def test_execute_nested_derived_current(self): @@ -215,5 +214,5 @@ def test_execute_nested_derived_current(self): "152": "at" } - actual = execute(parse_tree) + actual = self.executor.execute(parse_tree) self.assertEqual(expected, actual) diff --git a/tests/unit/transform/test_interpolate.py b/tests/unit/transform/test_interpolate.py index e7715de..e21629e 100644 --- a/tests/unit/transform/test_interpolate.py +++ b/tests/unit/transform/test_interpolate.py @@ -1,7 +1,8 @@ import unittest -from app.definitions import Template, Transforms, ParseTree, BuildSpecError -from app.transform.interpolate import interpolate, expand_nested_transforms, invert_post_transforms, map_template +from app.definitions.spec import Template, Transforms, ParseTree, BuildSpecError +from app.services.transform.interpolate import (interpolate, expand_nested_transforms, + invert_post_transforms, map_template) class InterpolateTests(unittest.TestCase): diff --git a/tests/unit/transform/test_populate.py b/tests/unit/transform/test_populate.py index 9f86dce..58adc3b 100644 --- a/tests/unit/transform/test_populate.py +++ b/tests/unit/transform/test_populate.py @@ -1,7 +1,7 @@ import unittest -from app.definitions import ParseTree -from app.transform.populate import populate_mappings, resolve_value_fields +from app.definitions.spec import ParseTree +from app.services.transform.populate import populate_mappings, resolve_value_fields class ImplicitValueTests(unittest.TestCase): diff --git a/tests/unit/transform/test_tree_walker.py b/tests/unit/transform/test_tree_walker.py index 2ae3807..057df64 100644 --- a/tests/unit/transform/test_tree_walker.py +++ b/tests/unit/transform/test_tree_walker.py @@ -1,7 +1,8 @@ import unittest -from app.definitions import ParseTree, Field -from app.transform.tree_walker import TreeWalker +from app.definitions.spec import ParseTree +from app.definitions.input import Field +from app.services.transform.tree_walker import TreeWalker class TreeWalkerTests(unittest.TestCase):