diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 18f8eb480c..2a0bd9d143 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -62,8 +62,7 @@ updates: - "/lambdas/recordprocessor" - "/lambdas/redis_sync" - "/lambdas/shared" - - "/tests/e2e" - - "/tests/e2e_batch" + - "/tests/e2e_automation" schedule: interval: "daily" open-pull-requests-limit: 1 diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 7555451a0d..de20f85219 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -18,41 +18,50 @@ jobs: # Technically the first step is not a pre-requisite - sandbox backend deployment is handled by APIM # Stipulating this condition simply makes it more likely the environment will be ready when tests are invoked needs: [deploy-internal-dev-backend] - uses: ./.github/workflows/run-e2e-tests.yml + uses: ./.github/workflows/run-e2e-automation-tests.yml with: apigee_environment: internal-dev-sandbox environment: dev sub_environment: internal-dev-sandbox + service_under_test: all + suite_to_run: sandbox secrets: APIGEE_PASSWORD: ${{ secrets.APIGEE_PASSWORD }} APIGEE_BASIC_AUTH_TOKEN: ${{ secrets.APIGEE_BASIC_AUTH_TOKEN }} APIGEE_OTP_KEY: ${{ secrets.APIGEE_OTP_KEY }} + CIS2_E2E_USERNAME: ${{ secrets.CIS2_E2E_USERNAME }} STATUS_API_KEY: ${{ secrets.STATUS_API_KEY }} run-sandbox-tests: needs: [run-internal-dev-sandbox-tests] - uses: ./.github/workflows/run-e2e-tests.yml + uses: ./.github/workflows/run-e2e-automation-tests.yml with: apigee_environment: sandbox environment: dev sub_environment: sandbox + service_under_test: all + suite_to_run: sandbox secrets: APIGEE_PASSWORD: ${{ secrets.APIGEE_PASSWORD }} APIGEE_BASIC_AUTH_TOKEN: ${{ secrets.APIGEE_BASIC_AUTH_TOKEN }} APIGEE_OTP_KEY: ${{ secrets.APIGEE_OTP_KEY }} + CIS2_E2E_USERNAME: ${{ secrets.CIS2_E2E_USERNAME }} STATUS_API_KEY: ${{ secrets.STATUS_API_KEY }} run-internal-dev-tests: needs: [deploy-internal-dev-backend] - uses: ./.github/workflows/run-e2e-tests.yml + uses: ./.github/workflows/run-e2e-automation-tests.yml with: apigee_environment: internal-dev environment: dev sub_environment: internal-dev + service_under_test: all + suite_to_run: smoke secrets: APIGEE_PASSWORD: ${{ secrets.APIGEE_PASSWORD }} APIGEE_BASIC_AUTH_TOKEN: ${{ secrets.APIGEE_BASIC_AUTH_TOKEN }} APIGEE_OTP_KEY: ${{ secrets.APIGEE_OTP_KEY }} + CIS2_E2E_USERNAME: ${{ secrets.CIS2_E2E_USERNAME }} STATUS_API_KEY: ${{ secrets.STATUS_API_KEY }} deploy-higher-dev-envs: @@ -72,13 +81,21 @@ jobs: strategy: matrix: sub_environment_name: [ref, internal-qa] - uses: ./.github/workflows/run-e2e-tests.yml + include: + - sub_environment_name: ref + required_test_suite: proxy_smoke + - sub_environment_name: internal-qa + required_test_suite: smoke + uses: ./.github/workflows/run-e2e-automation-tests.yml with: apigee_environment: ${{ matrix.sub_environment_name }} environment: dev sub_environment: ${{ matrix.sub_environment_name }} + service_under_test: all + suite_to_run: ${{ matrix.required_test_suite }} secrets: APIGEE_PASSWORD: ${{ secrets.APIGEE_PASSWORD }} APIGEE_BASIC_AUTH_TOKEN: ${{ secrets.APIGEE_BASIC_AUTH_TOKEN }} APIGEE_OTP_KEY: ${{ secrets.APIGEE_OTP_KEY }} + CIS2_E2E_USERNAME: ${{ secrets.CIS2_E2E_USERNAME }} STATUS_API_KEY: ${{ secrets.STATUS_API_KEY }} diff --git a/.github/workflows/pr-deploy-and-test.yml b/.github/workflows/pr-deploy-and-test.yml index 19b6863f41..4bfac98a25 100644 --- a/.github/workflows/pr-deploy-and-test.yml +++ b/.github/workflows/pr-deploy-and-test.yml @@ -13,18 +13,26 @@ jobs: environment: dev sub_environment: pr-${{github.event.pull_request.number}} - run-e2e-tests: + run-e2e-automation-tests: needs: [deploy-pr-backend] strategy: matrix: apigee_environment_name: [internal-dev, internal-dev-sandbox] - uses: ./.github/workflows/run-e2e-tests.yml + include: + - apigee_environment_name: internal-dev + required_test_suite: smoke + - apigee_environment_name: internal-dev-sandbox + required_test_suite: sandbox + uses: ./.github/workflows/run-e2e-automation-tests.yml with: apigee_environment: ${{ matrix.apigee_environment_name }} environment: dev sub_environment: pr-${{github.event.pull_request.number}} + service_under_test: all + suite_to_run: ${{ matrix.required_test_suite }} secrets: APIGEE_PASSWORD: ${{ secrets.APIGEE_PASSWORD }} APIGEE_BASIC_AUTH_TOKEN: ${{ secrets.APIGEE_BASIC_AUTH_TOKEN }} APIGEE_OTP_KEY: ${{ secrets.APIGEE_OTP_KEY }} + CIS2_E2E_USERNAME: ${{ secrets.CIS2_E2E_USERNAME }} STATUS_API_KEY: ${{ secrets.STATUS_API_KEY }} diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-automation-tests.yml similarity index 56% rename from .github/workflows/run-e2e-tests.yml rename to .github/workflows/run-e2e-automation-tests.yml index 5491c07452..6331dc72cb 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-automation-tests.yml @@ -1,4 +1,4 @@ -name: Run e2e Tests +name: FHIR API and Batch Automation - E2E Tests on: workflow_call: @@ -12,6 +12,12 @@ on: sub_environment: required: true type: string + service_under_test: + required: true + type: string + suite_to_run: + required: true + type: string secrets: APIGEE_PASSWORD: required: true @@ -19,6 +25,8 @@ on: required: true APIGEE_OTP_KEY: required: true + CIS2_E2E_USERNAME: + required: true STATUS_API_KEY: required: true workflow_dispatch: @@ -33,19 +41,35 @@ on: - internal-qa - int - ref - - prod environment: type: string description: Select the backend environment options: - dev - preprod - - prod sub_environment: type: string description: Set the sub environment name e.g. pr-xxx, or green/blue in higher environments + service_under_test: + description: Select the service you want to test + required: true + type: choice + options: + - fhir_api + - batch + - all + suite_to_run: + description: Select the suite you would like to run + default: "functional" + type: choice + options: + - smoke + - functional + - sandbox + - proxy_smoke env: + APIGEE_AUTH_ENV: ${{ inputs.apigee_environment == 'int' && inputs.apigee_environment || 'internal-dev' }} APIGEE_ENVIRONMENT: ${{ inputs.apigee_environment }} ENVIRONMENT: ${{ inputs.environment }} SUB_ENVIRONMENT: ${{ inputs.sub_environment }} @@ -60,11 +84,9 @@ jobs: contents: read runs-on: ubuntu-latest environment: ${{ inputs.apigee_environment }} - outputs: - # Workaround for environment-level variables being unavailable in `jobs..if`. - RUN_BATCH_E2E_TESTS: ${{ vars.RUN_BATCH_E2E_TESTS }} steps: - name: Wait for API to be available + if: github.event_name != 'workflow_dispatch' run: | endpoint="" if [[ ${APIGEE_ENVIRONMENT} =~ "prod" ]]; then @@ -106,16 +128,18 @@ jobs: exit 1 fi - e2e-tests: + e2e-automation-tests: permissions: id-token: write - contents: read + checks: write + contents: write runs-on: ubuntu-latest needs: [wait-for-deployment] environment: ${{ inputs.apigee_environment }} env: APIGEE_USERNAME: ${{ vars.APIGEE_USERNAME }} - TF_OUTPUTS_REQUIRED: ${{ vars.RUN_FULL_E2E_TESTS == 'true' || vars.RUN_PROXY_E2E_TESTS == 'true' }} + TF_OUTPUTS_REQUIRED: ${{ inputs.suite_to_run != 'sandbox' }} + steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -146,11 +170,7 @@ jobs: if: ${{ env.TF_OUTPUTS_REQUIRED == 'true' }} working-directory: infrastructure/instance run: | - echo "IMMS_DELTA_TABLE_NAME=$(make -s output name=imms_delta_table_name)" >> $GITHUB_ENV echo "AWS_DOMAIN_NAME=$(make -s output name=service_domain_name)" >> $GITHUB_ENV - echo "DYNAMODB_TABLE_NAME=$(make -s output name=dynamodb_table_name)" >> $GITHUB_ENV - echo "AWS_SQS_QUEUE_NAME=$(make -s output name=aws_sqs_queue_name)" >> $GITHUB_ENV - echo "AWS_SNS_TOPIC_NAME=$(make -s output name=aws_sns_topic_name)" >> $GITHUB_ENV - name: Install poetry run: pip install poetry==2.1.4 @@ -159,21 +179,21 @@ jobs: with: python-version: 3.11 cache: "poetry" - cache-dependency-path: tests/e2e/poetry.lock + cache-dependency-path: tests/e2e_automation/poetry.lock - name: Install e2e test dependencies - working-directory: tests/e2e + working-directory: tests/e2e_automation run: poetry install --no-root - name: Get Apigee access token - if: ${{ vars.RUN_FULL_E2E_TESTS == 'true' }} - working-directory: tests/e2e + if: inputs.apigee_environment != 'int' + working-directory: tests/e2e_automation env: APIGEE_PASSWORD: ${{ secrets.APIGEE_PASSWORD }} APIGEE_BASIC_AUTH_TOKEN: ${{ secrets.APIGEE_BASIC_AUTH_TOKEN }} APIGEE_OTP_KEY: ${{ secrets.APIGEE_OTP_KEY }} run: | - CODE=$(poetry run python utils/compute_totp_code.py "$APIGEE_OTP_KEY") + CODE=$(poetry run python utilities/compute_totp_code.py "$APIGEE_OTP_KEY") echo "::add-mask::$CODE" echo "Requesting access token from Apigee..." @@ -187,55 +207,73 @@ jobs: echo "::add-mask::$token" echo "APIGEE_ACCESS_TOKEN=$token" >> $GITHUB_ENV - - name: Run proxy e2e test suite - if: ${{ vars.RUN_PROXY_E2E_TESTS == 'true' }} - working-directory: tests/e2e - run: poetry run python -m unittest test_proxy - - - name: Run sandbox e2e test suite - if: ${{ vars.RUN_SANDBOX_E2E_TESTS == 'true' }} - working-directory: tests/e2e - run: poetry run python -m unittest test_proxy.TestProxyHealthcheck - - - name: Run full e2e test suite - if: ${{ vars.RUN_FULL_E2E_TESTS == 'true' }} - working-directory: tests/e2e - run: poetry run python -m unittest - - batch-e2e-tests: - permissions: - id-token: write - contents: read - needs: [wait-for-deployment, e2e-tests] - # Only actually depend on wait-for-deployment, but run after e2e-tests - if: ${{ !cancelled() && needs.wait-for-deployment.result == 'success' && needs.wait-for-deployment.outputs.RUN_BATCH_E2E_TESTS == 'true' }} - runs-on: ubuntu-latest - environment: ${{ inputs.apigee_environment }} - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + - name: Run Pytest-BDD ${{ inputs.service_under_test }} tests with the ${{ inputs.suite_to_run }} filter + working-directory: tests/e2e_automation + env: + S3_env: ${{ inputs.sub_environment }} + auth_url: https://${{ env.APIGEE_AUTH_ENV }}.api.service.nhs.uk/oauth2-mock/authorize + token_url: https://${{ env.APIGEE_AUTH_ENV }}.api.service.nhs.uk/oauth2-mock/token + baseUrl: https://${{ inputs.apigee_environment }}.api.service.nhs.uk/${{env.SERVICE_BASE_PATH}} + callback_url: "https://oauth.pstmn.io/v1/callback" + username: ${{ secrets.CIS2_E2E_USERNAME }} + scope: "nhs-cis2" + USE_STATIC_APPS: ${{ inputs.apigee_environment == 'int' && 'True' || 'False' }} + Postman_Auth_client_Id: ${{ secrets.Postman_Auth_client_Id }} # The following static app values are only needed in INT + Postman_Auth_client_Secret: ${{ secrets.Postman_Auth_client_Secret }} + RAVS_client_Id: ${{ secrets.RAVS_client_Id }} + RAVS_client_Secret: ${{ secrets.RAVS_client_Secret }} + MAVIS_client_Id: ${{ secrets.MAVIS_client_Id }} + MAVIS_client_Secret: ${{ secrets.MAVIS_client_Secret }} + EMIS_client_Id: ${{ secrets.OPTUM_client_Id }} + EMIS_client_Secret: ${{ secrets.OPTUM_client_Secret }} + TPP_client_Id: ${{ secrets.TPP_client_Id }} + TPP_client_Secret: ${{ secrets.TPP_client_Secret }} + SONAR_client_Id: ${{ secrets.SONAR_client_Id }} + SONAR_client_Secret: ${{ secrets.SONAR_client_Secret }} + MEDICUS_client_Id: ${{ secrets.MEDICUS_client_Id }} + MEDICUS_client_Secret: ${{ secrets.MEDICUS_client_Secret }} + aws_token_refresh: "False" + TEST_PATH: ${{ inputs.service_under_test == 'batch' && 'features/batchTests' || inputs.service_under_test == 'fhir_api' && 'features/APITests' || 'features' }} + TEST_FILTER: ${{ inputs.suite_to_run == 'proxy_smoke' && 'Status_feature' || inputs.suite_to_run }} + run: poetry run pytest "$TEST_PATH" -m "$TEST_FILTER" --junitxml=output/test-results.xml --alluredir=output/allure-results - - name: Connect to AWS - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 + - uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 + if: always() with: - aws-region: eu-west-2 - role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/auto-ops - role-session-name: github-actions + name: BDD Test Summary + path: "**/output/test-results.xml" + reporter: java-junit + fail-on-error: false - - name: Install poetry - run: pip install poetry==2.1.4 + - name: Load test report history + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + if: always() + continue-on-error: true + with: + ref: gh-pages + path: gh-pages - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 + - name: Build Allure report + if: always() + uses: simple-elf/allure-report-action@53ebb757a2097edc77c53ecef4d454fc2f2f774c with: - python-version: 3.11 - cache: "poetry" - cache-dependency-path: tests/e2e_batch/poetry.lock + allure_results: tests/e2e_automation/output/allure-results + gh_pages: gh-pages + allure_report: allure-report + allure_history: allure-history - - name: Install e2e test dependencies - working-directory: tests/e2e_batch - run: poetry install --no-root + - name: Publish Allure report to GitHub Pages + if: always() + uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: gh-pages + publish_dir: allure-history - - name: Run batch e2e test suite - working-directory: tests/e2e_batch - env: - ENVIRONMENT: ${{ inputs.sub_environment }} - run: poetry run python -m unittest -c -v + - name: Add link to Allure report + if: always() + uses: actions/github-script@v6 + with: + script: | + const url = `https://${context.repo.owner}.github.io/${context.repo.repo}/`; + core.summary.addHeading('Allure Report').addLink('View Report', url).write(); diff --git a/.gitignore b/.gitignore index 2c2444ac20..7879dae0e2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ newman/ **/*.iml __pycache__/ +.pytest_cache/ .venv/ .env .envrc @@ -30,3 +31,8 @@ openapi.json devtools/volume/ **/.coverage +**/test-results.xml +allure-results/ +allure-report/ +**/debugLog.log +batch_files_directory/ diff --git a/Makefile b/Makefile index 9507029d5d..58b432c2ab 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL=/usr/bin/env bash -euo pipefail PYTHON_PROJECT_DIRS_WITH_UNIT_TESTS = lambdas/backend lambdas/ack_backend lambdas/batch_processor_filter lambdas/delta_backend lambdas/filenameprocessor lambdas/id_sync lambdas/mesh_processor lambdas/mns_subscription lambdas/recordforwarder lambdas/recordprocessor lambdas/redis_sync lambdas/shared -PYTHON_PROJECT_DIRS = tests/e2e tests/e2e_batch quality_checks $(PYTHON_PROJECT_DIRS_WITH_UNIT_TESTS) +PYTHON_PROJECT_DIRS = tests/e2e_automation quality_checks $(PYTHON_PROJECT_DIRS_WITH_UNIT_TESTS) .PHONY: install lint format format-check clean publish oas build-proxy release initialise-all-python-venvs update-all-python-dependencies run-all-python-unit-tests build-all-docker-images diff --git a/README.md b/README.md index 57bb4f65b2..49c1fc70c4 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,9 @@ See https://nhsd-confluence.digital.nhs.uk/display/APM/Glossary. ### Tests -| Folder | Description | -| ----------- | ------------------------------------------------------------------------------------ | -| `e2e` | End-to-end tests executed during PR pipelines. | -| `e2e_batch` | E2E tests specifically for batch-related functionality, also run in the PR pipeline. | +| Folder | Description | +| ---------------- | ----------------------------------------------------------------------------- | +| `e2e_automation` | End-to-end tests executed during PR pipelines using the pytest-bdd framework. | --- @@ -143,7 +142,7 @@ Steps: ### Setting up a virtual environment with poetry -The steps below must be performed in each Lambda function folder and e2e folder to ensure the environment is correctly configured. +The steps below must be performed in each Lambda function folder and e2e_automation folder to ensure the environment is correctly configured. For detailed instructions on running individual Lambdas, refer to the README.md files located inside each respective Lambda folder. diff --git a/immunisation-fhir-api.code-workspace b/immunisation-fhir-api.code-workspace index 7de6b0eb11..30671ba75c 100644 --- a/immunisation-fhir-api.code-workspace +++ b/immunisation-fhir-api.code-workspace @@ -40,10 +40,7 @@ "path": "lambdas/shared", }, { - "path": "tests/e2e", - }, - { - "path": "tests/e2e_batch", + "path": "tests/e2e_automation", }, ], "settings": {}, diff --git a/sonar-project.properties b/sonar-project.properties index 6d2749826f..6e7479ae02 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.projectKey=NHSDigital_immunisation-fhir-api sonar.organization=nhsdigital sonar.host.url=https://sonarcloud.io sonar.python.version=3.11 -sonar.exclusions=**/e2e/**,**/e2e_batch/**,**/devtools/**,**/proxies/**,**/utilities/scripts/**,**/infrastructure/account/**,**/infrastructure/instance/**,**/infrastructure/grafana/**,**/terraform_aws_backup/**,**/tests/** +sonar.exclusions=**/devtools/**,**/proxies/**,**/utilities/scripts/**,**/infrastructure/account/**,**/infrastructure/instance/**,**/infrastructure/grafana/**,**/terraform_aws_backup/**,**/tests/** sonar.coverage.exclusions=lambdas/shared/src/common/models/batch_constants.py sonar.python.coverage.reportPaths=backend-coverage.xml,delta-coverage.xml,ack-lambda-coverage.xml,filenameprocessor-coverage.xml,recordforwarder-coverage.xml,recordprocessor-coverage.xml,mesh_processor-coverage.xml,redis_sync-coverage.xml,mns_subscription-coverage.xml,id_sync-coverage.xml,shared-coverage.xml,batchprocessorfilter-coverage.xml sonar.cpd.exclusions=**/Dockerfile diff --git a/tests/e2e/.env.default b/tests/e2e/.env.default deleted file mode 100644 index 64fc55f710..0000000000 --- a/tests/e2e/.env.default +++ /dev/null @@ -1,5 +0,0 @@ -APIGEE_USERNAME=x@y.com -PROXY_NAME=immunisation-fhir-api-pr-000 -APIGEE_ENVIRONMENT=internal-dev -SERVICE_BASE_PATH=immunisation-fhir-api-pr-000 -SOURCE_COMMIT_ID=0000000000000000000000000000000000000000 diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore deleted file mode 100644 index e9ecd6ec84..0000000000 --- a/tests/e2e/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -*.key -*.pem -*.pub -*.iml -.well-known -.keys -# default poetry gitignore -*.pyc - -# Packages -*.egg -/*.egg-info -/dist/* -build -_build -.cache -*.so - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.pytest_cache - -.DS_Store -.idea/* -.python-version -.vscode/* - - -/setup.cfg -MANIFEST.in -/setup.py -/docs/site/* -.mypy_cache - -.venv -/releases/* -pip-wheel-metadata -/poetry.toml - -poetry/core/* diff --git a/tests/e2e/Makefile b/tests/e2e/Makefile deleted file mode 100644 index 3ec63edf5a..0000000000 --- a/tests/e2e/Makefile +++ /dev/null @@ -1,39 +0,0 @@ --include .env - -APIGEE_ACCESS_TOKEN ?= $(shell export SSO_LOGIN_URL=https://login.apigee.com && eval get_token -u $(APIGEE_USERNAME)) -AWS_DOMAIN_NAME=https://$(shell make -C ../../instance -s output name=service_domain_name || true) -DYNAMODB_TABLE_NAME=$(shell make -C ../../instance -s output name=dynamodb_table_name || true) -IMMS_DELTA_TABLE_NAME=$(shell make -C ../../instance -s output name=imms_delta_table_name || true) -AWS_SQS_QUEUE_NAME=$(shell make -C ../../instance -s output name=aws_sqs_queue_name || true) -AWS_SNS_TOPIC_NAME=$(shell make -C ../../instance -s output name=aws_sns_topic_name || true) - -cmd = APIGEE_ACCESS_TOKEN=$(APIGEE_ACCESS_TOKEN) APIGEE_USERNAME=$(APIGEE_USERNAME) IMMS_DELTA_TABLE_NAME=$(IMMS_DELTA_TABLE_NAME) AWS_DOMAIN_NAME=$(AWS_DOMAIN_NAME) DYNAMODB_TABLE_NAME=$(DYNAMODB_TABLE_NAME) AWS_SQS_QUEUE_NAME=$(AWS_SQS_QUEUE_NAME) AWS_SNS_TOPIC_NAME=$(AWS_SNS_TOPIC_NAME) poetry run python -m unittest - -run-immunization: - $(cmd) discover -v -c -p 'test_*_immunization.py' - -run-delta-immunization: - $(cmd) discover -v -c -p 'test_delta_immunization.py' - -run-authorization: - $(cmd) -v -c test_authorization.py - -run-wait-for-deployment: - $(cmd) -v -c test_deployment.py - -run-proxy: - $(cmd) -v -c test_proxy.py - -run-smoketest: - $(cmd) -c -v -k test_proxy.TestProxyHealthcheck -k test_deployment - -run: - $(cmd) - -run-match-%: - $(cmd) -v -c -k $* - -file = immunisation-fhir-api-local -key-pair: - openssl genrsa -out .keys/$(file).key 4096 - openssl rsa -in .keys/$(file).key -pubout -outform PEM -out .keys/$(file).key.pub diff --git a/tests/e2e/README.md b/tests/e2e/README.md deleted file mode 100644 index 53334da0c8..0000000000 --- a/tests/e2e/README.md +++ /dev/null @@ -1,90 +0,0 @@ -## End-to-end Tests - -This directory contains end-to-end tests. Except for certain files, the majority of the tests hit the proxy (Apigee) and assert the response. - -## Setting up e2e tests to run locally - -1. Follow the instructions in the root level README.md to setup the [dependencies](../README.md#environment-setup) and create a [virtual environment](../README.md#) for this folder (`e2e`). - -2. Install the [get_token] utility provided by Apigee. It is required for authenticating with the Apigee platform. Please make sure you have an Apigee account for non-prod to be able to run these e2e tests. - -3. Add the following values in the `.env` file and set the desired PR number. If there is already an `.env` file make sure that you only have the values specified below. - - ``` - APIGEE_USERNAME={your-apigee-email} - APIGEE_ENVIRONMENT=internal-dev - PROXY_NAME=immunisation-fhir-api-pr-100 - SERVICE_BASE_PATH=immunisation-fhir-api/FHIR/R4-pr-100 - ``` - - There are other environment variables that are used, but these are the minimum amount for running tests locally. Apart from the first 4 items from the table below, you can ignore the rest of them. This will cause a few test failures, but it's safe to ignore them (locally). - - | Name | Example | Description | - | -------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------- | - | `APIGEE_USERNAME` | your-nhs-email@nhs.net | Your NHS email address, used to authenticate with Apigee. | - | `APIGEE_ENVIRONMENT` | internal-dev | The Apigee environment you are targeting (e.g., `internal-dev`, `prod`, etc.). | - | `PROXY_NAME` | immunisation-fhir-api-pr-100 | The name of the Apigee proxy you want to target. You can find this in the Apigee UI. | - | `SERVICE_BASE_PATH` | immunisation-fhir-api/FHIR/R4-pr-100 | The base path for the proxy. This can be found in the "Overview" section of the Apigee UI. | - | `STATUS_API_KEY` | secret | Used to test the `_status` endpoint. If not set, that specific test will fail (can be ignored locally). | - | `AWS_PROFILE` | apim-dev | Some operations require the AWS CLI. This profile is used for AWS authentication. | - | `AWS_DOMAIN_NAME` | https://pr-100.imms.dev.vds.platform.nhs.uk | The domain pointing to the backend deployment, used for testing mTLS. Can be ignored locally. | - -4. If you prefer to skip Terraform initialization, you can configure the `Makefile` to work without it. This step is optional — if Terraform has already been initialized, you can proceed to the next step. - - **Note:** The `Makefile` includes environment variables that are dynamically set when you run its commands. These variables are populated by scripts that retrieve information about AWS resources deployed to the specified environment. - - For local development, you can simplify the setup by modifying the configuration to point directly to resource names, instead of relying on Terraform initialization. - - ``` - APIGEE_ACCESS_TOKEN ?= $(shell export SSO_LOGIN_URL=https://login.apigee.com && eval get_token -u $(APIGEE_USERNAME)) - AWS_DOMAIN_NAME="" - DYNAMODB_TABLE_NAME=imms-internal-dev-imms-events - IMMS_DELTA_TABLE_NAME=imms-internal-dev-delta - AWS_SQS_QUEUE_NAME=imms-internal-dev-delta-dlq - AWS_SNS_TOPIC_NAME=imms-internal-dev-delta-sns - ``` - -5. The `Makefile` in this directory provides commands for running various sets of tests. The most important one for local development is `test-immunization`, which tests each Immunization operation such as create, read, delete, etc. To run all tests, use: `make run`. - -## Tests - -Each test follows a certain pattern. Each file contains a closely related feature. Each test class bundles related tests for that specific category. The first test is always the happy-path test. For backend operations, this first test, will be executed for each method of authentication i.e. `ApplicationRestricted`, `Cis2` and `NhsLogin`. The docstring in each test method contains a BDD style short summary. - -Sending a request to the proxy involves a few steps. First we need to create an Apigee product and then create an Apigee -app associated with that product. This app may need certain custom attributes depending on the authentication method. -All -the steps are put away in `utils/base_test.py` in a parent class. Unless you want to test certain scenarios, generally -the `setUpClass` and `tearDownClass` will set everything up for you. - -### Implementation - -This section contains information about the implementation. The source code can be put into three categories. - -#### test files - -Every single file in the parent directory is a test module. See section "Tests" for more info. - -#### lib directory - -This directory contains everything related to the test setup. The code in this directory has no knowledge of your project. -It doesn't even have the knowledge of the test framework. This is to make it completely stand-alone. You can copy/paste this -directory to another project without changing a line. If you are thinking of adding new functionality to it then keep it -that way. - -The content of this directory can be broken down into three categories: - -- **apigee:** Everything you need to set up Apigee app, product and authentication -- **authentication:** Contains everything you need to perform proxy authentication. It covers all three types of - authentication -- **env:** The utilities in the `lib` directory never assumes configurations. You need to pass them directly to create - an instance of the required tool. The `env.py` file is to make assumptions about the source of the configuration. - When making changes to this file keep two things in mind. 1- reduce the amount of config by convention over - configuration - 2- only look for the config when your code actually needs it. - -#### utils - -The files in this directory are test utilities, but they are still project agnostics. They don't know -anything about your particular project. Think of this directory more like a higher level wrapper around `lib`. -The most important file in this directory is the `base_test.py` file, which contains the test setup and teardown logic -related to common e2e tests. diff --git a/tests/e2e/lib/apigee.py b/tests/e2e/lib/apigee.py deleted file mode 100644 index 7ea11c86de..0000000000 --- a/tests/e2e/lib/apigee.py +++ /dev/null @@ -1,199 +0,0 @@ -import inspect -from dataclasses import asdict, dataclass, field -from enum import StrEnum -from typing import List - -import requests - - -class ApigeeOrg(StrEnum): - PROD = "nhsd-prod" - NON_PROD = "nhsd-nonprod" - - -class ApigeeEnv(StrEnum): - INTERNAL_DEV = "internal-dev" - INTERNAL_QA = "internal-qa" - INTERNAL_DEV_SANDBOX = "internal-dev-sandbox" - SANDBOX = "sandbox" - INT = "int" - REF = "ref" - PROD = "prod" - - -@dataclass -class ApigeeError(RuntimeError): - message: str - - -@dataclass -class ApigeeConfig: - username: str - access_token: str - env: ApigeeEnv = ApigeeEnv.INTERNAL_DEV - host: str = "https://api.enterprise.apigee.com" - org: ApigeeOrg = ApigeeOrg.NON_PROD - - def base_url(self): - return f"{self.host}/v1/organizations/{self.org.value}" - - -@dataclass -class ApigeeApp: - """Data object to create an apigee app or decode json response""" - - name: str - appId: str = None - credentials: List[dict] = field(default_factory=lambda: []) - attributes: List[dict] = field(default_factory=lambda: []) - apiProducts: List[str] = field(default_factory=lambda: []) - callbackUrl: str = "https://oauth.pstmn.io/v1/callback" - scopes: List[str] = field(default_factory=lambda: []) - status: str = "approved" - - def add_product(self, product_name: str): - self.apiProducts.append(product_name) - - def add_attribute(self, k: str, v: str): - self.attributes.append({"name": k, "value": v}) - - def set_display_name(self, name: str): - self.attributes.append({"name": "DisplayName", "value": name}) - - def get_client_id(self) -> str: - if not self.credentials: - raise RuntimeError("You need to first create an app before reading its credentials") - return self.credentials[0]["consumerKey"] - - def get_client_secret(self) -> str: - if not self.credentials: - raise RuntimeError("You need to first create an app before reading its credentials") - return self.credentials[0]["consumerSecret"] - - def dict(self): - return asdict(self) - - @classmethod - def from_dict(cls, data): - # Only consider keys that are in the class definition - return cls(**{k: v for k, v in data.items() if k in inspect.signature(cls).parameters}) - - -@dataclass -class ApigeeProduct: - """Data object to create an apigee product""" - - name: str - apiResources: List[str] = field(default_factory=lambda: []) - approvalType: str = "auto" - attributes: List[dict] = field(default_factory=lambda: []) - description: str = "My API product" - displayName: str = "My API product" - environments: List[str] = field(default_factory=lambda: [ApigeeEnv.INTERNAL_DEV.value]) - proxies: List[str] = field(default_factory=lambda: []) - scopes: List[str] = field(default_factory=lambda: []) - - def add_proxy(self, proxy_name: str, base_path: str): - self.proxies.append(proxy_name) - self.apiResources.append(base_path) - - def dict(self): - return asdict(self) - - @classmethod - def from_dict(cls, data): - # Only consider keys that are in the class definition - return cls(**{k: v for k, v in data.items() if k in inspect.signature(cls).parameters}) - - -class ApigeeService: - def __init__(self, config: ApigeeConfig): - self.base_url = config.base_url() - self.username = config.username - self.default_headers = {"Authorization": f"Bearer {config.access_token}"} - - def get_applications(self) -> dict: - params = {"email": self.username} - resource = f"developers/{self.username}/apps" - return self._get(resource, params) - - def create_application(self, app: ApigeeApp) -> dict: - resource = f"developers/{self.username}/apps" - return self._create(resource, app.dict()) - - def delete_application(self, name: str) -> dict: - resource = f"developers/{self.username}/apps/{name}" - return self._delete(resource) - - def get_app_attribute(self, app_name: str, attr_name: str) -> dict: - resource = f"/developers/{self.username}/apps/{app_name}/attributes/{attr_name}" - return self._get(resource, {}) - - def create_app_attribute(self, app_name: str, attr_name: str, value: str) -> dict: - resource = f"/developers/{self.username}/apps/{app_name}/attributes/{attr_name}" - body = {"name": attr_name, "value": value} - return self._create(resource, body) - - def delete_app_attribute(self, app_name: str, attr_name: str) -> dict: - resource = f"/developers/{self.username}/apps/{app_name}/attributes/{attr_name}" - return self._delete(resource) - - def get_product(self, name: str) -> dict: - resource = f"apiproducts/{name}" - return self._get(resource, None) - - def create_product(self, product: ApigeeProduct) -> dict: - resource = "apiproducts" - return self._create(resource, product.dict()) - - def add_proxy_to_product(self, product_name: str, proxy_name: str) -> dict: - product = self.get_product(product_name) - product["proxies"].append(proxy_name) - - resource = f"apiproducts/{product_name}" - return self._update(resource, product) - - def delete_product(self, name: str): - resource = f"apiproducts/{name}" - return self._delete(resource) - - def _get(self, path: str, params: dict = None) -> dict: - url = f"{self.base_url}/{path}" - resp = requests.get(url=url, params=params, headers=self.default_headers) - if resp.status_code != 200: - raise ApigeeError( - f"GET request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}" - ) - return resp.json() - - def _create(self, path: str, body: dict) -> dict: - url = f"{self.base_url}/{path}" - resp = requests.post(url=url, json=body, headers=self.default_headers) - if resp.status_code != 200 and resp.status_code != 201: - raise ApigeeError( - f"POST request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}" - ) - return resp.json() - - def _update(self, path: str, body: dict) -> dict: - url = f"{self.base_url}/{path}" - resp = requests.put(url=url, json=body, headers=self.default_headers) - if resp.status_code != 200: - raise ApigeeError( - f"PUT request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}" - ) - return resp.json() - - def _delete(self, path: str) -> dict: - url = f"{self.base_url}/{path}" - resp = requests.delete(url=url, headers=self.default_headers) - # 404 is a valid response for delete - if resp.status_code != 200 and resp.status_code != 404: - raise ApigeeError( - f"DELETE request to {resp.url} failed with status_code: {resp.status_code}, " - f"Reason: {resp.reason} and Content: {resp.text}" - ) - return resp.json() diff --git a/tests/e2e/lib/authentication.py b/tests/e2e/lib/authentication.py deleted file mode 100644 index c79024e73e..0000000000 --- a/tests/e2e/lib/authentication.py +++ /dev/null @@ -1,177 +0,0 @@ -import uuid -from abc import ABC, abstractmethod -from dataclasses import dataclass -from enum import Enum -from time import time -from urllib.parse import parse_qs, urlparse - -import jwt -import requests -from lxml import html - - -@dataclass -class AuthenticationError(RuntimeError): - message: str - - -class AuthType(str, Enum): - APP_RESTRICTED = "ApplicationRestricted" - NHS_LOGIN = "NhsLogin" - CIS2 = "Cis2" - - -class BaseAuthentication(ABC): - """interface for Apigee app authentication. ApigeeService uses this to get access token""" - - @abstractmethod - def get_access_token(self): - pass - - -@dataclass -class AppRestrictedCredentials: - client_id: str - kid: str - private_key_content: str - expiry_seconds: int = 30 - - -@dataclass -class UserRestrictedCredentials: - client_id: str - client_secret: str - callback_url: str = "https://oauth.pstmn.io/v1/callback" - - -@dataclass -class LoginUser: - username: str - password: str = "" - - -class AppRestrictedAuthentication(BaseAuthentication): - def __init__(self, auth_url: str, config: AppRestrictedCredentials): - self.expiry = config.expiry_seconds - self.private_key = config.private_key_content - self.client_id = config.client_id - self.kid = config.kid - self.token_url = f"{auth_url}/token" - - def __str__(self): - return AuthType.APP_RESTRICTED.value - - def get_access_token(self): - now = int(time()) - claims = { - "iss": self.client_id, - "sub": self.client_id, - "aud": self.token_url, - "iat": now, - "exp": now + self.expiry, - "jti": str(uuid.uuid4()), - } - _jwt = jwt.encode(claims, self.private_key, algorithm="RS512", headers={"kid": self.kid}) - - headers = {"Content-Type": "application/x-www-form-urlencoded"} - data = { - "grant_type": "client_credentials", - "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - "client_assertion": _jwt, - } - token_response = requests.post(self.token_url, data=data, headers=headers) - - if token_response.status_code != 200: - raise AuthenticationError(f"ApplicationRestricted token POST request failed\n{token_response.text}") - - return token_response.json().get("access_token") - - -class AuthCodeFlow: - def __init__(self, base_auth_url, app_credentials: UserRestrictedCredentials) -> None: - self.app_creds = app_credentials - self.auth_url = f"{base_auth_url}/authorize" - self.token_url = f"{base_auth_url}/token" - - @staticmethod - def extract_code(response) -> str: - qs = urlparse(response.history[-1].headers["Location"]).query - auth_code = parse_qs(qs)["code"] - if isinstance(auth_code, list): - # in case there's multiple, this was a bug at one stage - auth_code = auth_code[0] - - return auth_code - - def get_access_token(self, scope: str, user_cred: LoginUser) -> str: - login_session = requests.session() - - client_id = self.app_creds.client_id - client_secret = self.app_creds.client_secret - callback_url = self.app_creds.callback_url - username = user_cred.username - - # Step1: login page - authorize_resp = login_session.get( - self.auth_url, - params={ - "client_id": client_id, - "redirect_uri": callback_url, - "response_type": "code", - "scope": scope, - "state": str(uuid.uuid4()), - }, - ) - assert authorize_resp.status_code == 200, authorize_resp.text - - # Step2: Submit the login form - tree = html.fromstring(authorize_resp.content.decode()) - auth_form = tree.forms[0] - - form_url = auth_form.action - form_data = {"username": username} - - code_resp = login_session.post(url=form_url, data=form_data) - assert code_resp.status_code == 200, code_resp.text - - # Step3: extract code from redirect url - auth_code = self.extract_code(code_resp) - - # Step4: Post the code to get access token - token_resp = login_session.post( - self.token_url, - data={ - "grant_type": "authorization_code", - "code": auth_code, - "redirect_uri": callback_url, - "client_id": client_id, - "client_secret": client_secret, - }, - ) - assert token_resp.status_code == 200, token_resp.text - - return token_resp.json()["access_token"] - - -class NhsLoginAuthentication(BaseAuthentication): - def __str__(self): - return AuthType.NHS_LOGIN.value - - def get_access_token(self) -> str: - # TODO(NhsLogin_AMB-1923) add NHSLogin - pass - - -class Cis2Authentication(BaseAuthentication): - def __init__(self, auth_url: str, config: UserRestrictedCredentials, default_user: LoginUser): - self.code_flow = AuthCodeFlow(auth_url, config) - self.user = default_user - - def __str__(self): - return AuthType.CIS2.value - - def set_user(self, user: LoginUser): - self.user = user - - def get_access_token(self) -> str: - return self.code_flow.get_access_token("nhs-cis2", self.user) diff --git a/tests/e2e/lib/env.py b/tests/e2e/lib/env.py deleted file mode 100644 index 14f10c8180..0000000000 --- a/tests/e2e/lib/env.py +++ /dev/null @@ -1,116 +0,0 @@ -import logging -import os -import subprocess - -from .apigee import ApigeeEnv -from .authentication import AppRestrictedCredentials - -"""use functions in this module to get configs that can be read from environment variables or external processes""" - - -def get_apigee_username(): - if username := os.getenv("APIGEE_USERNAME"): - return username - else: - logging.error('environment variable "APIGEE_USERNAME" is required') - - -def get_apigee_env() -> ApigeeEnv: - if env := os.getenv("APIGEE_ENVIRONMENT"): - try: - return ApigeeEnv(env) - except ValueError: - logging.error(f'the environment variable "APIGEE_ENVIRONMENT: {env}" is invalid') - else: - logging.warning( - 'the environment variable "APIGEE_ENVIRONMENT" is empty, falling back to the default value: "internal-dev"' - ) - return ApigeeEnv.INTERNAL_DEV - - -def get_apigee_access_token(username: str = None): - if access_token := os.getenv("APIGEE_ACCESS_TOKEN"): - return access_token - - if username := username or get_apigee_username(): - env = os.environ.copy() - env["SSO_LOGIN_URL"] = env.get("SSO_LOGIN_URL", "https://login.apigee.com") - try: - res = subprocess.run( - ["get_token", "-u", username], - env=env, - stdout=subprocess.PIPE, - text=True, - ) - return res.stdout.strip() - except FileNotFoundError: - raise RuntimeError( - "Make sure you install apigee's get_token utility and make sure it's in your PATH. " - "Follow: https://docs.apigee.com/api-platform/system-administration/using-gettoken" - ) - - -def get_default_app_restricted_credentials() -> AppRestrictedCredentials: - client_id = os.getenv("DEFAULT_CLIENT_ID") - kid = os.getenv("DEFAULT_APP_ID") - if not client_id or not kid: - raise RuntimeError('Both "DEFAULT_CLIENT_ID" and "DEFAULT_APP_ID" are required') - private_key = get_private_key() - - return AppRestrictedCredentials(client_id=client_id, kid=kid, private_key_content=private_key) - - -def get_private_key_path() -> str: - if not os.getenv("APP_RESTRICTED_PRIVATE_KEY_PATH"): - raise RuntimeError('"APP_RESTRICTED_PRIVATE_KEY_PATH" is required') - return os.getenv("APP_RESTRICTED_PRIVATE_KEY_PATH") - - -def get_public_key_path() -> str: - if not os.getenv("APP_RESTRICTED_PUBLIC_KEY_PATH"): - raise RuntimeError('"APP_RESTRICTED_PUBLIC_KEY_PATH" is required') - return os.getenv("APP_RESTRICTED_PUBLIC_KEY_PATH") - - -def get_private_key(file_path: str = None) -> str: - file_path = file_path if file_path else get_private_key_path() - with open(file_path, "r") as f: - return f.read() - - -def get_auth_url(apigee_env: ApigeeEnv = None) -> str: - if not apigee_env: - apigee_env = get_apigee_env() - - if apigee_env == ApigeeEnv.PROD: - return "https://api.service.nhs.uk/oauth2" - else: - return f"https://{apigee_env.value}.api.service.nhs.uk/oauth2-mock" - - -def get_proxy_name() -> str: - if not os.getenv("PROXY_NAME"): - raise RuntimeError('"PROXY_NAME" is required') - return os.getenv("PROXY_NAME") - - -def get_service_base_path(apigee_env: ApigeeEnv = None) -> str: - if not os.getenv("SERVICE_BASE_PATH"): - raise RuntimeError('"SERVICE_BASE_PATH" is required') - apigee_env = apigee_env if apigee_env else get_apigee_env() - - base_path = os.getenv("SERVICE_BASE_PATH") - if apigee_env.value == "prod": - return f"https://api.service.nhs.uk/{base_path}" - - return f"https://{apigee_env.value}.api.service.nhs.uk/{base_path}" - - -def get_status_endpoint_api_key() -> str: - if not os.getenv("STATUS_API_KEY"): - raise RuntimeError('"STATUS_API_KEY" is required') - return os.getenv("STATUS_API_KEY") - - -def get_source_commit_id() -> str: - return os.getenv("SOURCE_COMMIT_ID") diff --git a/tests/e2e/lib/jwks.py b/tests/e2e/lib/jwks.py deleted file mode 100644 index c8c169340e..0000000000 --- a/tests/e2e/lib/jwks.py +++ /dev/null @@ -1,76 +0,0 @@ -import base64 -import json -from dataclasses import dataclass - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa - - -@dataclass -class JwksData: - private_key: str - public_key: str - key_id: str - encoded_n: str = None - - def __init__(self, key_id: str, private_key_path: str = None, public_key_path: str = None): - """it generates everything that is required for jwks. If you are not passing private/public key path then - it will be generated dynamically""" - - self.key_id = key_id - if private_key_path is None and public_key_path is None: - self.private_key, self.public_key, self.encoded_n = _make_key_pair_n() - else: - with open(private_key_path, "r") as private_key: - self.private_key = private_key.read() - with open(public_key_path, "r") as public_key: - self.public_key = public_key.read() - - pub_key = serialization.load_pem_public_key(self.public_key.encode(), backend=default_backend()) - n = pub_key.public_numbers().n - n_bytes = n.to_bytes((n.bit_length() + 7) // 8, byteorder="big") - self.encoded_n = base64.urlsafe_b64encode(n_bytes).decode("utf-8") - - def get_jwk(self): - return { - "kty": "RSA", - "n": self.encoded_n, - "e": "AQAB", - "alg": "RS512", - "kid": self.key_id, - } - - def get_jwks_url(self, base_url: str) -> str: - jwks = json.dumps({"keys": [self.get_jwk()]}) - jwks_encoded = base64.urlsafe_b64encode(jwks.encode()).decode("utf-8") - return f"{base_url}/{jwks_encoded}" - - -def _make_key_pair_n(key_size=4096) -> (str, str, str): - private_key = rsa.generate_private_key(public_exponent=65537, key_size=key_size, backend=default_backend()) - - prv = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - - public_key = private_key.public_key() - pub = public_key.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.PKCS1) - - n = public_key.public_numbers().n - n_bytes = n.to_bytes((n.bit_length() + 7) // 8, byteorder="big") - n_encoded = base64.urlsafe_b64encode(n_bytes).decode("utf-8") - - return prv.decode(), pub.decode(), n_encoded - - -if __name__ == "__main__": - p = "/Users/jalal/tmp/imms-batch-key" - kid = "ecf6452b-96a5-44b9-95cd-6dae700e85a8" - pk = f"{p}/imms-batch.key" - pp = f"{p}/imms-batch.key.pub" - jwks_data = JwksData(kid, private_key_path=pk, public_key_path=pp) - print(jwks_data.get_jwk()) - print(jwks_data.get_jwks_url("https://api.service.nhs.uk/mock-jwks")) diff --git a/tests/e2e/poetry.lock b/tests/e2e/poetry.lock deleted file mode 100644 index acee0624fa..0000000000 --- a/tests/e2e/poetry.lock +++ /dev/null @@ -1,767 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. - -[[package]] -name = "boto3" -version = "1.42.26" -description = "The AWS SDK for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "boto3-1.42.26-py3-none-any.whl", hash = "sha256:f116cfbe7408e0a9153da363f134d2f1b5008f17ee86af104f0ce59a62be1833"}, - {file = "boto3-1.42.26.tar.gz", hash = "sha256:0fbcf1922e62d180f3644bc1139425821b38d93c1e6ec27409325d2ae86131aa"}, -] - -[package.dependencies] -botocore = ">=1.42.26,<1.43.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.16.0,<0.17.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.42.26" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "botocore-1.42.26-py3-none-any.whl", hash = "sha256:71171c2d09ac07739f4efce398b15a4a8bc8769c17fb3bc99625e43ed11ad8b7"}, - {file = "botocore-1.42.26.tar.gz", hash = "sha256:1c8855e3e811f015d930ccfe8751d4be295aae0562133d14b6f0b247cd6fd8d3"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} - -[package.extras] -crt = ["awscrt (==0.29.2)"] - -[[package]] -name = "certifi" -version = "2026.1.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, -] - -[[package]] -name = "cffi" -version = "2.0.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, -] - -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, -] - -[[package]] -name = "cryptography" -version = "46.0.3" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" -groups = ["dev"] -files = [ - {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, - {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, - {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, - {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, - {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, - {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, - {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, - {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, - {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, - {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, - {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, - {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, - {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, - {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, - {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, - {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, - {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, - {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, - {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, - {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, - {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "idna" -version = "3.11" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "jmespath" -version = "1.0.1" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "lxml" -version = "4.9.4" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" -groups = ["dev"] -files = [ - {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e214025e23db238805a600f1f37bf9f9a15413c7bf5f9d6ae194f84980c78722"}, - {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec53a09aee61d45e7dbe7e91252ff0491b6b5fee3d85b2d45b173d8ab453efc1"}, - {file = "lxml-4.9.4-cp27-cp27m-win32.whl", hash = "sha256:7d1d6c9e74c70ddf524e3c09d9dc0522aba9370708c2cb58680ea40174800013"}, - {file = "lxml-4.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:cb53669442895763e61df5c995f0e8361b61662f26c1b04ee82899c2789c8f69"}, - {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:647bfe88b1997d7ae8d45dabc7c868d8cb0c8412a6e730a7651050b8c7289cf2"}, - {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4d973729ce04784906a19108054e1fd476bc85279a403ea1a72fdb051c76fa48"}, - {file = "lxml-4.9.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:056a17eaaf3da87a05523472ae84246f87ac2f29a53306466c22e60282e54ff8"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aaa5c173a26960fe67daa69aa93d6d6a1cd714a6eb13802d4e4bd1d24a530644"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:647459b23594f370c1c01768edaa0ba0959afc39caeeb793b43158bb9bb6a663"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bdd9abccd0927673cffe601d2c6cdad1c9321bf3437a2f507d6b037ef91ea307"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:00e91573183ad273e242db5585b52670eddf92bacad095ce25c1e682da14ed91"}, - {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a602ed9bd2c7d85bd58592c28e101bd9ff9c718fbde06545a70945ffd5d11868"}, - {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de362ac8bc962408ad8fae28f3967ce1a262b5d63ab8cefb42662566737f1dc7"}, - {file = "lxml-4.9.4-cp310-cp310-win32.whl", hash = "sha256:33714fcf5af4ff7e70a49731a7cc8fd9ce910b9ac194f66eaa18c3cc0a4c02be"}, - {file = "lxml-4.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:d3caa09e613ece43ac292fbed513a4bce170681a447d25ffcbc1b647d45a39c5"}, - {file = "lxml-4.9.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:359a8b09d712df27849e0bcb62c6a3404e780b274b0b7e4c39a88826d1926c28"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:43498ea734ccdfb92e1886dfedaebeb81178a241d39a79d5351ba2b671bff2b2"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4855161013dfb2b762e02b3f4d4a21cc7c6aec13c69e3bffbf5022b3e708dd97"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c71b5b860c5215fdbaa56f715bc218e45a98477f816b46cfde4a84d25b13274e"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9a2b5915c333e4364367140443b59f09feae42184459b913f0f41b9fed55794a"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d82411dbf4d3127b6cde7da0f9373e37ad3a43e89ef374965465928f01c2b979"}, - {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:273473d34462ae6e97c0f4e517bd1bf9588aa67a1d47d93f760a1282640e24ac"}, - {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:389d2b2e543b27962990ab529ac6720c3dded588cc6d0f6557eec153305a3622"}, - {file = "lxml-4.9.4-cp311-cp311-win32.whl", hash = "sha256:8aecb5a7f6f7f8fe9cac0bcadd39efaca8bbf8d1bf242e9f175cbe4c925116c3"}, - {file = "lxml-4.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:c7721a3ef41591341388bb2265395ce522aba52f969d33dacd822da8f018aff8"}, - {file = "lxml-4.9.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:dbcb2dc07308453db428a95a4d03259bd8caea97d7f0776842299f2d00c72fc8"}, - {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:01bf1df1db327e748dcb152d17389cf6d0a8c5d533ef9bab781e9d5037619229"}, - {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e8f9f93a23634cfafbad6e46ad7d09e0f4a25a2400e4a64b1b7b7c0fbaa06d9d"}, - {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3f3f00a9061605725df1816f5713d10cd94636347ed651abdbc75828df302b20"}, - {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:953dd5481bd6252bd480d6ec431f61d7d87fdcbbb71b0d2bdcfc6ae00bb6fb10"}, - {file = "lxml-4.9.4-cp312-cp312-win32.whl", hash = "sha256:266f655d1baff9c47b52f529b5f6bec33f66042f65f7c56adde3fcf2ed62ae8b"}, - {file = "lxml-4.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:f1faee2a831fe249e1bae9cbc68d3cd8a30f7e37851deee4d7962b17c410dd56"}, - {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23d891e5bdc12e2e506e7d225d6aa929e0a0368c9916c1fddefab88166e98b20"}, - {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e96a1788f24d03e8d61679f9881a883ecdf9c445a38f9ae3f3f193ab6c591c66"}, - {file = "lxml-4.9.4-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:5557461f83bb7cc718bc9ee1f7156d50e31747e5b38d79cf40f79ab1447afd2d"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:fdb325b7fba1e2c40b9b1db407f85642e32404131c08480dd652110fc908561b"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d74d4a3c4b8f7a1f676cedf8e84bcc57705a6d7925e6daef7a1e54ae543a197"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ac7674d1638df129d9cb4503d20ffc3922bd463c865ef3cb412f2c926108e9a4"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:ddd92e18b783aeb86ad2132d84a4b795fc5ec612e3545c1b687e7747e66e2b53"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bd9ac6e44f2db368ef8986f3989a4cad3de4cd55dbdda536e253000c801bcc7"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bc354b1393dce46026ab13075f77b30e40b61b1a53e852e99d3cc5dd1af4bc85"}, - {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f836f39678cb47c9541f04d8ed4545719dc31ad850bf1832d6b4171e30d65d23"}, - {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9c131447768ed7bc05a02553d939e7f0e807e533441901dd504e217b76307745"}, - {file = "lxml-4.9.4-cp36-cp36m-win32.whl", hash = "sha256:bafa65e3acae612a7799ada439bd202403414ebe23f52e5b17f6ffc2eb98c2be"}, - {file = "lxml-4.9.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6197c3f3c0b960ad033b9b7d611db11285bb461fc6b802c1dd50d04ad715c225"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:7b378847a09d6bd46047f5f3599cdc64fcb4cc5a5a2dd0a2af610361fbe77b16"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:1343df4e2e6e51182aad12162b23b0a4b3fd77f17527a78c53f0f23573663545"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6dbdacf5752fbd78ccdb434698230c4f0f95df7dd956d5f205b5ed6911a1367c"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:506becdf2ecaebaf7f7995f776394fcc8bd8a78022772de66677c84fb02dd33d"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca8e44b5ba3edb682ea4e6185b49661fc22b230cf811b9c13963c9f982d1d964"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9d9d5726474cbbef279fd709008f91a49c4f758bec9c062dfbba88eab00e3ff9"}, - {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bbdd69e20fe2943b51e2841fc1e6a3c1de460d630f65bde12452d8c97209464d"}, - {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8671622256a0859f5089cbe0ce4693c2af407bc053dcc99aadff7f5310b4aa02"}, - {file = "lxml-4.9.4-cp37-cp37m-win32.whl", hash = "sha256:dd4fda67f5faaef4f9ee5383435048ee3e11ad996901225ad7615bc92245bc8e"}, - {file = "lxml-4.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6bee9c2e501d835f91460b2c904bc359f8433e96799f5c2ff20feebd9bb1e590"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:1f10f250430a4caf84115b1e0f23f3615566ca2369d1962f82bef40dd99cd81a"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b505f2bbff50d261176e67be24e8909e54b5d9d08b12d4946344066d66b3e43"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1449f9451cd53e0fd0a7ec2ff5ede4686add13ac7a7bfa6988ff6d75cff3ebe2"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4ece9cca4cd1c8ba889bfa67eae7f21d0d1a2e715b4d5045395113361e8c533d"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59bb5979f9941c61e907ee571732219fa4774d5a18f3fa5ff2df963f5dfaa6bc"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b1980dbcaad634fe78e710c8587383e6e3f61dbe146bcbfd13a9c8ab2d7b1192"}, - {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9ae6c3363261021144121427b1552b29e7b59de9d6a75bf51e03bc072efb3c37"}, - {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bcee502c649fa6351b44bb014b98c09cb00982a475a1912a9881ca28ab4f9cd9"}, - {file = "lxml-4.9.4-cp38-cp38-win32.whl", hash = "sha256:a8edae5253efa75c2fc79a90068fe540b197d1c7ab5803b800fccfe240eed33c"}, - {file = "lxml-4.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:701847a7aaefef121c5c0d855b2affa5f9bd45196ef00266724a80e439220e46"}, - {file = "lxml-4.9.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:f610d980e3fccf4394ab3806de6065682982f3d27c12d4ce3ee46a8183d64a6a"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aa9b5abd07f71b081a33115d9758ef6077924082055005808f68feccb27616bd"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:365005e8b0718ea6d64b374423e870648ab47c3a905356ab6e5a5ff03962b9a9"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:16b9ec51cc2feab009e800f2c6327338d6ee4e752c76e95a35c4465e80390ccd"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a905affe76f1802edcac554e3ccf68188bea16546071d7583fb1b693f9cf756b"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd814847901df6e8de13ce69b84c31fc9b3fb591224d6762d0b256d510cbf382"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91bbf398ac8bb7d65a5a52127407c05f75a18d7015a270fdd94bbcb04e65d573"}, - {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f99768232f036b4776ce419d3244a04fe83784bce871b16d2c2e984c7fcea847"}, - {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bb5bd6212eb0edfd1e8f254585290ea1dadc3687dd8fd5e2fd9a87c31915cdab"}, - {file = "lxml-4.9.4-cp39-cp39-win32.whl", hash = "sha256:88f7c383071981c74ec1998ba9b437659e4fd02a3c4a4d3efc16774eb108d0ec"}, - {file = "lxml-4.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:936e8880cc00f839aa4173f94466a8406a96ddce814651075f95837316369899"}, - {file = "lxml-4.9.4-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:f6c35b2f87c004270fa2e703b872fcc984d714d430b305145c39d53074e1ffe0"}, - {file = "lxml-4.9.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:606d445feeb0856c2b424405236a01c71af7c97e5fe42fbc778634faef2b47e4"}, - {file = "lxml-4.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1bdcbebd4e13446a14de4dd1825f1e778e099f17f79718b4aeaf2403624b0f7"}, - {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0a08c89b23117049ba171bf51d2f9c5f3abf507d65d016d6e0fa2f37e18c0fc5"}, - {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:232fd30903d3123be4c435fb5159938c6225ee8607b635a4d3fca847003134ba"}, - {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:231142459d32779b209aa4b4d460b175cadd604fed856f25c1571a9d78114771"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:520486f27f1d4ce9654154b4494cf9307b495527f3a2908ad4cb48e4f7ed7ef7"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:562778586949be7e0d7435fcb24aca4810913771f845d99145a6cee64d5b67ca"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a9e7c6d89c77bb2770c9491d988f26a4b161d05c8ca58f63fb1f1b6b9a74be45"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:786d6b57026e7e04d184313c1359ac3d68002c33e4b1042ca58c362f1d09ff58"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95ae6c5a196e2f239150aa4a479967351df7f44800c93e5a975ec726fef005e2"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:9b556596c49fa1232b0fff4b0e69b9d4083a502e60e404b44341e2f8fb7187f5"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cc02c06e9e320869d7d1bd323df6dd4281e78ac2e7f8526835d3d48c69060683"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:857d6565f9aa3464764c2cb6a2e3c2e75e1970e877c188f4aeae45954a314e0c"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c42ae7e010d7d6bc51875d768110c10e8a59494855c3d4c348b068f5fb81fdcd"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f10250bb190fb0742e3e1958dd5c100524c2cc5096c67c8da51233f7448dc137"}, - {file = "lxml-4.9.4.tar.gz", hash = "sha256:b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (==0.29.37)"] - -[[package]] -name = "mypy-boto3-dynamodb" -version = "1.42.3" -description = "Type annotations for boto3 DynamoDB 1.42.3 service generated with mypy-boto3-builder 8.12.0" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "mypy_boto3_dynamodb-1.42.3-py3-none-any.whl", hash = "sha256:ed339bb2a61131531a23d55b85e51fd5aed085fff2d6ce1c36f88988b3a9bef4"}, - {file = "mypy_boto3_dynamodb-1.42.3.tar.gz", hash = "sha256:6e390b7442eded84279fda6ddb8d8f14d0cfc52c8d16e64dab14e6239acb4a2a"}, -] - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.12\""} - -[[package]] -name = "oath" -version = "1.4.4" -description = "Python implementation of the three main OATH specifications: HOTP, TOTP and OCRA" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "oath-1.4.4-py3-none-any.whl", hash = "sha256:503092f388f041f91737f6b3bd5b83e8cf3f40c7d9bc87bcfbfac33e0ae6d685"}, - {file = "oath-1.4.4.tar.gz", hash = "sha256:bd6b20d20f2c4e3f53523ee900211dca75aeeca72f4f5a9fd8dcacc175fe1c0b"}, -] - -[[package]] -name = "pycparser" -version = "2.23" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, -] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[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 = "s3transfer" -version = "0.16.0" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe"}, - {file = "s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920"}, -] - -[package.dependencies] -botocore = ">=1.37.4,<2.0a.0" - -[package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] - -[[package]] -name = "simplejson" -version = "3.20.2" -description = "Simple, fast, extensible JSON encoder/decoder for Python" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5" -groups = ["main"] -files = [ - {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:11847093fd36e3f5a4f595ff0506286c54885f8ad2d921dfb64a85bce67f72c4"}, - {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d291911d23b1ab8eb3241204dd54e3ec60ddcd74dfcb576939d3df327205865"}, - {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:da6d16d7108d366bbbf1c1f3274662294859c03266e80dd899fc432598115ea4"}, - {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9ddf9a07694c5bbb4856271cbc4247cc6cf48f224a7d128a280482a2f78bae3d"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3a0d2337e490e6ab42d65a082e69473717f5cc75c3c3fb530504d3681c4cb40c"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8ba88696351ed26a8648f8378a1431223f02438f8036f006d23b4f5b572778fa"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:00bcd408a4430af99d1f8b2b103bb2f5133bb688596a511fcfa7db865fbb845e"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4fc62feb76f590ccaff6f903f52a01c58ba6423171aa117b96508afda9c210f0"}, - {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5"}, - {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d"}, - {file = "simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7"}, - {file = "simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53"}, - {file = "simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476"}, - {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f"}, - {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8"}, - {file = "simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413"}, - {file = "simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961"}, - {file = "simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c"}, - {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6"}, - {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259"}, - {file = "simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544"}, - {file = "simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54"}, - {file = "simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab"}, - {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86"}, - {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74"}, - {file = "simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769"}, - {file = "simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661"}, - {file = "simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608"}, - {file = "simplejson-3.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a135941a50795c934bdc9acc74e172b126e3694fe26de3c0c1bc0b33ea17e6ce"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ba488decb18738f5d6bd082018409689ed8e74bc6c4d33a0b81af6edf1c9f4"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d81f8e982923d5e9841622ff6568be89756428f98a82c16e4158ac32b92a3787"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdad497ccb1edc5020bef209e9c3e062a923e8e6fca5b8a39f0fb34380c8a66c"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a3f1db97bcd9fb592928159af7a405b18df7e847cbcc5682a209c5b2ad5d6b1"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:215b65b0dc2c432ab79c430aa4f1e595f37b07a83c1e4c4928d7e22e6b49a748"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:ece4863171ba53f086a3bfd87f02ec3d6abc586f413babfc6cf4de4d84894620"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4a76d7c47d959afe6c41c88005f3041f583a4b9a1783cf341887a3628a77baa0"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:e9b0523582a57d9ea74f83ecefdffe18b2b0a907df1a9cef06955883341930d8"}, - {file = "simplejson-3.20.2-cp36-cp36m-win32.whl", hash = "sha256:16366591c8e08a4ac76b81d76a3fc97bf2bcc234c9c097b48d32ea6bfe2be2fe"}, - {file = "simplejson-3.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:732cf4c4ac1a258b4e9334e1e40a38303689f432497d3caeb491428b7547e782"}, - {file = "simplejson-3.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6c3a98e21e5f098e4f982ef302ebb1e681ff16a5d530cfce36296bea58fe2396"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cf9ca1363dc3711c72f4ec7c1caed2bbd9aaa29a8d9122e31106022dc175c6"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:106762f8aedf3fc3364649bfe8dc9a40bf5104f872a4d2d86bae001b1af30d30"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b21659898b7496322e99674739193f81052e588afa8b31b6a1c7733d8829b925"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fa1db6a02bca88829f2b2057c76a1d2dc2fccb8c5ff1199e352f213e9ec719"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:156139d94b660448ec8a4ea89f77ec476597f752c2ff66432d3656704c66b40e"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:b2620ac40be04dff08854baf6f4df10272f67079f61ed1b6274c0e840f2e2ae1"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:9ccef5b5d3e3ac5d9da0a0ca1d2de8cf2b0fb56b06aa0ab79325fa4bcc5a1d60"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f526304c2cc9fd8b8d18afacb75bc171650f83a7097b2c92ad6a431b5d7c1b72"}, - {file = "simplejson-3.20.2-cp37-cp37m-win32.whl", hash = "sha256:e0f661105398121dd48d9987a2a8f7825b8297b3b2a7fe5b0d247370396119d5"}, - {file = "simplejson-3.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dab98625b3d6821e77ea59c4d0e71059f8063825a0885b50ed410e5c8bd5cb66"}, - {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b8205f113082e7d8f667d6cd37d019a7ee5ef30b48463f9de48e1853726c6127"}, - {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc8da64929ef0ff16448b602394a76fd9968a39afff0692e5ab53669df1f047f"}, - {file = "simplejson-3.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfe704864b5fead4f21c8d448a89ee101c9b0fc92a5f40b674111da9272b3a90"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ca7cbe7d2f423b97ed4e70989ef357f027a7e487606628c11b79667639dc84"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cec1868b237fe9fb2d466d6ce0c7b772e005aadeeda582d867f6f1ec9710cad"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:792debfba68d8dd61085ffb332d72b9f5b38269cda0c99f92c7a054382f55246"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e022b2c4c54cb4855e555f64aa3377e3e5ca912c372fa9e3edcc90ebbad93dce"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5de26f11d5aca575d3825dddc65f69fdcba18f6ca2b4db5cef16f41f969cef15"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e2162b2a43614727ec3df75baeda8881ab129824aa1b49410d4b6c64f55a45b4"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e11a1d6b2f7e72ca546bdb4e6374b237ebae9220e764051b867111df83acbd13"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:daf7cd18fe99eb427fa6ddb6b437cfde65125a96dc27b93a8969b6fe90a1dbea"}, - {file = "simplejson-3.20.2-cp38-cp38-win32.whl", hash = "sha256:da795ea5f440052f4f497b496010e2c4e05940d449ea7b5c417794ec1be55d01"}, - {file = "simplejson-3.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:6a4b5e7864f952fcce4244a70166797d7b8fd6069b4286d3e8403c14b88656b6"}, - {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bf76512ccb07d47944ebdca44c65b781612d38b9098566b4bb40f713fc4047"}, - {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:214e26acf2dfb9ff3314e65c4e168a6b125bced0e2d99a65ea7b0f169db1e562"}, - {file = "simplejson-3.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fb1259ca9c385b0395bad59cdbf79535a5a84fb1988f339a49bfbc57455a35a"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34e028a2ba8553a208ded1da5fa8501833875078c4c00a50dffc33622057881"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b538f9d9e503b0dd43af60496780cb50755e4d8e5b34e5647b887675c1ae9fee"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab998e416ded6c58f549a22b6a8847e75a9e1ef98eb9fbb2863e1f9e61a4105b"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8f1c307edf5fbf0c6db3396c5d3471409c4a40c7a2a466fbc762f20d46601a"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a7bbac80bdb82a44303f5630baee140aee208e5a4618e8b9fde3fc400a42671"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5ef70ec8fe1569872e5a3e4720c1e1dcb823879a3c78bc02589eb88fab920b1f"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11c09c99253a74c36925d461c86ea25f0140f3b98ff678322734ddc0f038d7"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:66f7c78c6ef776f8bd9afaad455e88b8197a51e95617bcc44b50dd974a7825ba"}, - {file = "simplejson-3.20.2-cp39-cp39-win32.whl", hash = "sha256:619ada86bfe3a5aa02b8222ca6bfc5aa3e1075c1fb5b3263d24ba579382df472"}, - {file = "simplejson-3.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:44a6235e09ca5cc41aa5870a952489c06aa4aee3361ae46daa947d8398e57502"}, - {file = "simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017"}, - {file = "simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649"}, -] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[metadata] -lock-version = "2.1" -python-versions = "~3.11" -content-hash = "69b8228378e3fdea7b39ad3f86ff5a9b99ed7f75bbb050b23e422afd866806f3" diff --git a/tests/e2e/pyproject.toml b/tests/e2e/pyproject.toml deleted file mode 100644 index 71982da3f0..0000000000 --- a/tests/e2e/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[tool.poetry] -name = "e2e" -version = "0.1.0" -description = "End-to-end tests for immunization-fhir-api" -authors = ["Your Name "] -license = "MIT" -readme = "README.md" - -[tool.poetry.dependencies] -python = "~3.11" -boto3 = "^1.41.0" -mypy-boto3-dynamodb = "^1.41.0" -simplejson = "^3.20.2" - -[tool.poetry.group.dev.dependencies] -requests = "^2.32.5" -pyjwt = "^2.10.1" -cryptography = "^46.0.3" -lxml = "~4.9.0" -oath = "^1.4.4" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/tests/e2e/test_authorization.py b/tests/e2e/test_authorization.py deleted file mode 100644 index 4ad8192c90..0000000000 --- a/tests/e2e/test_authorization.py +++ /dev/null @@ -1,338 +0,0 @@ -import unittest -import uuid -from typing import Set - -from lib.apigee import ApigeeApp -from lib.authentication import ( - AppRestrictedAuthentication, - Cis2Authentication, - LoginUser, -) -from lib.env import get_auth_url, get_proxy_name, get_service_base_path -from utils.authorization import Permission, app_full_access -from utils.base_test import ImmunizationBaseTest -from utils.constants import cis2_user, valid_nhs_number1 -from utils.factories import make_app_restricted_app, make_cis2_app -from utils.immunisation_api import ImmunisationApi -from utils.mappings import VaccineTypes -from utils.resource import generate_imms_resource - - -@unittest.skip("Skipping this entire test suite for now") -class TestApplicationRestrictedAuthorization(ImmunizationBaseTest): - my_app: ApigeeApp - my_imms_api: ImmunisationApi - - def make_app(self, permissions: Set[Permission], vaxx_type_perms: Set = None): - # The super class gives us everything we need, which is useful for test setup; - # however, we need to create a new app with required permissions. - # This new app and its api are called my_app and my_imms_api, i.e., app under test - display_name = f"test-{get_proxy_name()}" - - app_data = ApigeeApp(name=str(uuid.uuid4()), apiProducts=[self.product.name]) - app_data.set_display_name(display_name) - self.my_app, app_res_cfg = make_app_restricted_app(self.apigee_service, app_data, permissions, vaxx_type_perms) - - app_res_auth = AppRestrictedAuthentication(get_auth_url(), app_res_cfg) - base_url = get_service_base_path() - - self.my_imms_api = ImmunisationApi(base_url, app_res_auth) - - # Runs after each individual test method in a test class. - # It’s used to clean up resources that were initialized specifically for a single test. - def tearDown(self): - self.apigee_service.delete_application(self.my_app.name) - self.my_imms_api.cleanup_test_records() - self.default_imms_api.cleanup_test_records() - - def test_get_imms_authorised(self): - """it should get Immunization if app has immunization:read permission""" - imms_id = self.default_imms_api.create_immunization_resource() - self.make_app({Permission.READ}) - # When - response = self.my_imms_api.get_immunization_by_id(imms_id) - # Then - self.assertEqual(response.status_code, 200, response.text) - - def test_get_imms_unauthorised(self): - """it should not get Immunization if app doesn't have immunization:read permission""" - perms = app_full_access(exclude={Permission.READ}) - self.make_app(perms) - # When - response = self.my_imms_api.get_immunization_by_id("id-doesn't-matter", expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_create_imms_authorised(self): - """it should create Immunization if app has immunization:create permission""" - self.make_app({Permission.CREATE}) - # When - imms = generate_imms_resource() - response = self.my_imms_api.create_immunization(imms) - # Then - self.assertEqual(response.status_code, 201, response.text) - - def test_create_imms_unauthorised_vaxx(self): - """it should not create Immunization if app does not have the correct vaccine permission""" - self.make_app({Permission.CREATE}, {"flu:create"}) - # When - imms = generate_imms_resource() - response = self.my_imms_api.create_immunization(imms, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_create_imms_unauthorised(self): - """it should not create Immunization if app doesn't immunization:create permission""" - perms = app_full_access(exclude={Permission.CREATE}) - self.make_app(perms) - # When - imms = generate_imms_resource() - result = self.my_imms_api.create_immunization(imms, expected_status_code=403) - # Then - self.assertEqual(result.status_code, 403, result.text) - - def test_update_imms_authorised(self): - """it should update Immunization if app has immunization:update and immunization:create permission""" - imms = generate_imms_resource() - imms_id = self.default_imms_api.create_immunization_resource(imms) - imms["id"] = imms_id - - self.make_app({Permission.CREATE, Permission.UPDATE}) - # When - response = self.my_imms_api.update_immunization(imms_id, imms) - # Then - self.assertEqual(response.status_code, 200, response.text) - - def test_update_imms_unauthorised(self): - """it should not update Immunization if app doesn't immunization:update permission""" - perms = app_full_access(exclude={Permission.UPDATE}) - self.make_app(perms) - # When - response = self.my_imms_api.update_immunization("doesn't-matter", {}, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_update_imms_unauthorised_2(self): - """it should not update Immunization if app doesn't immunization:create permission""" - imms = generate_imms_resource() - imms_id = self.default_imms_api.create_immunization_resource(imms) - imms["id"] = imms_id - - perms = app_full_access(exclude={Permission.CREATE}) - self.make_app(perms) - # When - response = self.my_imms_api.update_immunization(imms_id, imms, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_delete_imms_authorised(self): - """it should delete Immunization if app has immunization:delete permission""" - imms_id = self.default_imms_api.create_immunization_resource() - self.make_app({Permission.DELETE}) - # When - response = self.my_imms_api.delete_immunization(imms_id) - # Then - self.assertEqual(response.status_code, 204, response.text) - - def test_delete_imms_unauthorised(self): - """it should not delete Immunization if app doesn't have immunization:delete permission""" - perms = app_full_access(exclude={Permission.DELETE}) - self.make_app(perms) - # When - response = self.my_imms_api.delete_immunization("doesn't-matter", expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_search_imms_authorised(self): - """it should search Immunization if app has immunization:search permission""" - mmr = generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr) - _ = self.default_imms_api.create_immunization_resource(mmr) - - self.make_app({Permission.SEARCH}) - # When - response = self.my_imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.mmr) - # Then - self.assertEqual(response.status_code, 200, response.text) - - def test_search_imms_unauthorised(self): - """it should not search Immunization if app doesn't immunization:search permission""" - perms = app_full_access(exclude={Permission.SEARCH}) - self.make_app(perms) - # When - response = self.my_imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.mmr, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_search_imms_unauthorised_vax(self): - """it should not search Immunization if app does not have proper vax permissions""" - mmr = generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr) - _ = self.default_imms_api.create_immunization_resource(mmr) - - self.make_app({Permission.SEARCH}, {"flu:read"}) - # When - response = self.my_imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.mmr, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - -@unittest.skip("Skipping this entire test suite for now") -class TestCis2Authorization(ImmunizationBaseTest): - my_app: ApigeeApp - my_imms_api: ImmunisationApi - - def make_app(self, permissions: Set[Permission], vaxx_type_perms: Set = None): - # The super class gives us everything we need, which is useful for test setup; - # however, we need to create a new app with required permissions. - # This new app and its api are called my_app and my_imms_api, i.e., app under test - display_name = f"test-{get_proxy_name()}" - - app_data = ApigeeApp(name=str(uuid.uuid4()), apiProducts=[self.product.name]) - app_data.set_display_name(display_name) - self.my_app, app_res_cfg = make_cis2_app(self.apigee_service, app_data, permissions, vaxx_type_perms) - - app_res_auth = Cis2Authentication(get_auth_url(), app_res_cfg, LoginUser(username=cis2_user)) - base_url = get_service_base_path() - - self.my_imms_api = ImmunisationApi(base_url, app_res_auth) - - # Runs after each individual test method in a test class. - # It’s used to clean up resources that were initialized specifically for a single test. - def tearDown(self): - self.apigee_service.delete_application(self.my_app.name) - self.my_imms_api.cleanup_test_records() - self.default_imms_api.cleanup_test_records() - - def test_get_imms_authorised(self): - """it should get Immunization if app has immunization:read permission""" - imms_id = self.default_imms_api.create_immunization_resource() - self.make_app({Permission.READ}) - # When - response = self.my_imms_api.get_immunization_by_id(imms_id) - # Then - self.assertEqual(response.status_code, 200, response.text) - - def test_get_imms_unauthorised(self): - """it should not get Immunization if app doesn't have immunization:read permission""" - perms = app_full_access(exclude={Permission.READ}) - self.make_app(perms) - # When - response = self.my_imms_api.get_immunization_by_id("id-doesn't-matter", expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_get_imms__unauthorised_vaxx(self): - """it should not get Immunization if app does not have the correct vaccine permission""" - imms_id = self.default_imms_api.create_immunization_resource() - self.make_app({Permission.READ}, {"flu:create"}) - # When - response = self.my_imms_api.get_immunization_by_id(imms_id, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_create_imms_authorised(self): - """it should create Immunization if app has immunization:create permission""" - self.make_app({Permission.CREATE}) - # When - imms = generate_imms_resource() - response = self.my_imms_api.create_immunization(imms) - # Then - self.assertEqual(response.status_code, 201, response.text) - - def test_create_imms_unauthorised(self): - """it should not create Immunization if app doesn't immunization:create permission""" - perms = app_full_access(exclude={Permission.CREATE}) - self.make_app(perms) - # When - imms = generate_imms_resource() - result = self.my_imms_api.create_immunization(imms, expected_status_code=403) - # Then - self.assertEqual(result.status_code, 403, result.text) - - def test_create_imms_unauthorised_vaxx(self): - """it should not create Immunization if app does not have the correct vaccine permission""" - self.make_app({Permission.CREATE}, {"flu:create"}) - # When - imms = generate_imms_resource() - response = self.my_imms_api.create_immunization(imms, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_update_imms_authorised(self): - """it should update Immunization if app has the immunization:update and immunization:create permission""" - imms = generate_imms_resource() - imms_id = self.default_imms_api.create_immunization_resource(imms) - imms["id"] = imms_id - - self.make_app({Permission.CREATE, Permission.UPDATE}) - # When - response = self.my_imms_api.update_immunization(imms_id, imms) - # Then - self.assertEqual(response.status_code, 200, response.text) - - def test_update_imms_unauthorised(self): - """it should not update Immunization if app doesn't have the immunization:update permission""" - perms = app_full_access(exclude={Permission.UPDATE}) - self.make_app(perms) - # When - response = self.my_imms_api.update_immunization("doesn't-matter", {}, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_update_imms_unauthorised_vaxx(self): - """it should not update Immunization if app does not have the correct vaccine permission""" - imms = generate_imms_resource() - imms_id = self.default_imms_api.create_immunization_resource(imms) - imms["id"] = imms_id - - self.make_app({Permission.CREATE, Permission.UPDATE}, {"flu:create"}) - # When - response = self.my_imms_api.update_immunization(imms_id, imms, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_delete_imms_authorised(self): - """it should delete Immunization if app has immunization:delete permission""" - imms_id = self.default_imms_api.create_immunization_resource() - self.make_app({Permission.DELETE}) - # When - response = self.my_imms_api.delete_immunization(imms_id) - # Then - self.assertEqual(response.status_code, 204, response.text) - - def test_delete_imms_unauthorised(self): - """it should not delete Immunization if app doesn't have immunization:delete permission""" - perms = app_full_access(exclude={Permission.DELETE}) - self.make_app(perms) - # When - response = self.my_imms_api.delete_immunization("doesn't-matter", expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_delete_imms__unauthorised_vaxx(self): - """it should not delete Immunization if app does not have the correct vaccine permission""" - imms_id = self.default_imms_api.create_immunization_resource() - self.make_app({Permission.READ}, {"flu:create"}) - # When - response = self.my_imms_api.delete_immunization(imms_id, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) - - def test_search_imms_authorised(self): - """it should search Immunization if app has immunization:search permission""" - mmr = generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr) - _ = self.default_imms_api.create_immunization_resource(mmr) - - self.make_app({Permission.SEARCH}) - # When - response = self.my_imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.mmr) - # Then - self.assertEqual(response.status_code, 200, response.text) - - def test_search_imms_unauthorised(self): - """it should not search Immunization if app doesn't have the immunization:search permission""" - perms = app_full_access(exclude={Permission.SEARCH}) - self.make_app(perms) - # When - response = self.my_imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.mmr, expected_status_code=403) - # Then - self.assertEqual(response.status_code, 403, response.text) diff --git a/tests/e2e/test_create_immunization.py b/tests/e2e/test_create_immunization.py deleted file mode 100644 index f1114bf57d..0000000000 --- a/tests/e2e/test_create_immunization.py +++ /dev/null @@ -1,168 +0,0 @@ -from utils.base_test import ImmunizationBaseTest -from utils.resource import generate_imms_resource, get_full_row_from_identifier - - -class TestCreateImmunization(ImmunizationBaseTest): - def test_create_imms(self): - """it should create a FHIR Immunization resource""" - for imms_api in self.imms_apis: - with self.subTest(imms_api): - # Given - immunizations = [ - generate_imms_resource(), - generate_imms_resource(sample_data_file_name="completed_rsv_immunization_event"), - ] - - for immunization in immunizations: - # When - response = imms_api.create_immunization(immunization) - - # Then - self.assertEqual(response.status_code, 201, response.text) - self.assertEqual(response.text, "") - self.assertIn("Location", response.headers) - - def test_non_unique_identifier(self): - """ - it should give 422 if the identifier is not unique, even if the original imms event has been deleted and/ or - reinstated - """ - # Set up - imms = generate_imms_resource() - imms_id = self.default_imms_api.create_immunization_resource(imms) - res = self.default_imms_api.get_immunization_by_id(imms_id) - self.assertEqual(res.status_code, 200) - - # Check that duplicate CREATE request is rejected - self.assert_operation_outcome( - self.default_imms_api.create_immunization(imms, expected_status_code=422), - 422, - ) - self.assertEqual(res.headers["E-Tag"], "1") - - # Check that duplicate CREATE request is rejected after the event is updated - imms["id"] = imms_id # Imms fhir resource should include the id for update - self.default_imms_api.update_immunization(imms_id, imms) - self.assertEqual(res.status_code, 200) - del imms["id"] # Imms fhir resource should not include an id for create - self.assert_operation_outcome( - self.default_imms_api.create_immunization(imms, expected_status_code=422), - 422, - ) - - # Check that duplicate CREATE request is rejected after the event is updated then deleted - self.default_imms_api.delete_immunization(imms_id) - self.assertEqual( - self.default_imms_api.get_immunization_by_id(imms_id, expected_status_code=404).status_code, - 404, - ) - self.assert_operation_outcome( - self.default_imms_api.create_immunization(imms, expected_status_code=422), - 422, - ) - - # Check that duplicate CREATE request is rejected after the event is updated then deleted then reinstated - imms["id"] = imms_id # Imms fhir resource should include the id for update - self.default_imms_api.update_immunization(imms_id, imms, headers={"E-Tag": "2"}) - res = self.default_imms_api.get_immunization_by_id(imms_id) - self.assertEqual(res.status_code, 200) - del imms["id"] # Imms fhir resource should not include an id for create - self.assert_operation_outcome( - self.default_imms_api.create_immunization(imms, expected_status_code=422), - 422, - ) - self.assertEqual(res.headers["E-Tag"], "3") - - def test_invalid_nhs_number(self): - """it should reject the request if nhs-number does not conform to MOD11""" - invalid_nhs_number = "9434765911" # check digit 1 doesn't match result (9) - imms = generate_imms_resource(nhs_number=invalid_nhs_number) - - response = self.default_imms_api.create_immunization(imms, expected_status_code=400) - self.assertEqual(response.status_code, 400) - - def test_valid_nhs_number_no_pds(self): - """it should accept the request if nhs-number is valid but was previously known to have been - notified as non-existent by PDS""" - valid_nhs_number = "9462206376" - imms = generate_imms_resource(nhs_number=valid_nhs_number) - - response = self.default_imms_api.create_immunization(imms) - - self.assertEqual(response.status_code, 201, response.text) - self.assertEqual(response.text, "") - self.assertTrue("Location" in response.headers) - - def test_validation(self): - """it should validate Immunization""" - # NOTE: This e2e test is here to prove validation logic is wired to the backend. - # validation is thoroughly unit tested in the backend code - imms = generate_imms_resource() - invalid_datetime = "2020-12-32" - imms["occurrenceDateTime"] = invalid_datetime - # When - response = self.default_imms_api.create_immunization(imms, expected_status_code=400) - - # Then - self.assert_operation_outcome(response, 400, "occurrenceDateTime") - - def test_no_nhs_number(self): - """it should accept the request if nhs-number is missing""" - imms = generate_imms_resource() - del imms["contained"][1]["identifier"][0]["value"] - - response = self.default_imms_api.create_immunization(imms) - - self.assertEqual(response.status_code, 201, response.text) - self.assertEqual(response.text, "") - self.assertTrue("Location" in response.headers) - - # Check that nhs_number has been stored in IEDS as TBC - identifier = response.headers.get("location").split("/")[-1] - patient_pk = get_full_row_from_identifier(identifier).get("PatientPK") - self.assertEqual(patient_pk, "Patient#TBC") - - def test_no_patient_identifier(self): - """it should accept the request if patient identifier is missing""" - imms = generate_imms_resource() - del imms["contained"][1]["identifier"] - - response = self.default_imms_api.create_immunization(imms) - - self.assertEqual(response.status_code, 201, response.text) - self.assertEqual(response.text, "") - self.assertTrue("Location" in response.headers) - - # Check that nhs_number has been stored in IEDS as TBC - identifier = response.headers.get("location").split("/")[-1] - patient_pk = get_full_row_from_identifier(identifier).get("PatientPK") - self.assertEqual(patient_pk, "Patient#TBC") - - def test_create_imms_for_mandatory_fields_only(self): - """Test that data containing only the mandatory fields is accepted for create""" - imms = generate_imms_resource( - nhs_number=None, - sample_data_file_name="completed_covid_immunization_event_mandatory_fields_only", - ) - - # When - response = self.default_imms_api.create_immunization(imms) - - # Then - self.assertEqual(response.status_code, 201, response.text) - self.assertEqual(response.text, "") - self.assertTrue("Location" in response.headers) - - def test_create_imms_with_missing_mandatory_field(self): - """Test that data is rejected for create if one of the mandatory fields is missing""" - imms = generate_imms_resource( - nhs_number=None, - sample_data_file_name="completed_covid_immunization_event_mandatory_fields_only", - ) - del imms["primarySource"] - - # When - response = self.default_imms_api.create_immunization(imms, expected_status_code=400) - - # Then - self.assert_operation_outcome(response, 400, "primarySource is a mandatory field") diff --git a/tests/e2e/test_delete_immunization.py b/tests/e2e/test_delete_immunization.py deleted file mode 100644 index 18ba5e369c..0000000000 --- a/tests/e2e/test_delete_immunization.py +++ /dev/null @@ -1,37 +0,0 @@ -from utils.base_test import ImmunizationBaseTest -from utils.immunisation_api import parse_location -from utils.resource import generate_imms_resource - - -class TestDeleteImmunization(ImmunizationBaseTest): - def test_delete_imms(self): - """it should delete a FHIR Immunization resource""" - for imms_api in self.imms_apis: - with self.subTest(imms_api): - # Given - immunization_data_list = [ - generate_imms_resource(), - generate_imms_resource(sample_data_file_name="completed_rsv_immunization_event"), - ] - - created_ids = [] - for imms_data in immunization_data_list: - response = imms_api.create_immunization(imms_data) - self.assertEqual(response.status_code, 201) - created_id = parse_location(response.headers["Location"]) - created_ids.append(created_id) - - # When - for imms_id in created_ids: - delete_response = imms_api.delete_immunization(imms_id) - - # Then - self.assertEqual(delete_response.status_code, 204) - self.assertEqual(delete_response.text, "") - self.assertTrue("Location" not in delete_response.headers) - - def test_delete_immunization_already_deleted(self): - """it should return 404 when deleting a deleted resource""" - imms = self.default_imms_api.create_a_deleted_immunization_resource() - response = self.default_imms_api.delete_immunization(imms["id"], expected_status_code=404) - self.assert_operation_outcome(response, 404) diff --git a/tests/e2e/test_delta_immunization.py b/tests/e2e/test_delta_immunization.py deleted file mode 100644 index 89f63bb867..0000000000 --- a/tests/e2e/test_delta_immunization.py +++ /dev/null @@ -1,82 +0,0 @@ -import copy -import os -import time -from datetime import datetime - -from utils.base_test import ImmunizationBaseTest -from utils.immunisation_api import parse_location -from utils.resource import generate_imms_resource, get_dynamodb_table - - -class TestDeltaImmunization(ImmunizationBaseTest): - CREATE_OPERATION = "CREATE" - UPDATE_OPERATION = "UPDATE" - DELETE_OPERATION = "DELETE" - DPS_SOURCE = "DPS" - - def test_create_delta_imms(self): - """Should create,update,delete FHIR Immunization resource causing those resources to be stored in Delta table""" - imms_delta_table = get_dynamodb_table(os.getenv("IMMS_DELTA_TABLE_NAME")) - # Given - start_timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - # Creating two Imms Event, one of them will be updated and second of them will be deleted afterwards - # It should add 4 rows in Delta Storage table - create_update_imms = generate_imms_resource() - create_delete_imms = generate_imms_resource() - create_update_response = self.default_imms_api.create_immunization(create_update_imms) - - create_delete_response = self.default_imms_api.create_immunization(create_delete_imms) - assert create_update_response.status_code == 201 - assert create_delete_response.status_code == 201 - create_update_imms_id = parse_location(create_update_response.headers["Location"]) - - create_delete_imms_id = parse_location(create_delete_response.headers["Location"]) - - # When - update_payload = copy.deepcopy(create_update_imms) - update_payload["id"] = create_update_imms_id - update_payload["location"]["identifier"]["value"] = "Y11111" - create_update_response = self.default_imms_api.update_immunization(create_update_imms_id, update_payload) - self.assertEqual(create_update_response.status_code, 200) - - create_delete_response = self.default_imms_api.delete_immunization(create_delete_imms_id) - self.assertEqual(create_delete_response.status_code, 204) - - # Then - # Adding a delay of 30 seconds, because lambda can take some time to put records in Delta table - time.sleep(30) - end_timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - operations = [ - self.CREATE_OPERATION, - self.DELETE_OPERATION, - self.UPDATE_OPERATION, - ] - delta_imms_query_response = [] - for operation in operations: - expression_attribute_values = { - ":start": start_timestamp, - ":end": end_timestamp, - ":operation": operation, - ":DPS": self.DPS_SOURCE, - } - - key_condition_expression = "Operation = :operation AND DateTimeStamp BETWEEN :start AND :end " - expression_attribute_names = {"#Source": "Source"} # Define an alias for the reserved attribute name - is_not_DPS = "#Source <> :DPS" - response = imms_delta_table.query( - IndexName="SearchIndex", - KeyConditionExpression=key_condition_expression, - ExpressionAttributeValues=expression_attribute_values, - ExpressionAttributeNames=expression_attribute_names, - FilterExpression=is_not_DPS, - ) - delta_imms_query_response.extend((entity["ImmsID"], entity["Operation"]) for entity in response["Items"]) - if operation == self.CREATE_OPERATION: - self.assertTrue( - ((create_update_imms_id, self.CREATE_OPERATION) in delta_imms_query_response) - and ((create_delete_imms_id, self.CREATE_OPERATION) in delta_imms_query_response) - ) - elif operation == self.UPDATE_OPERATION: - self.assertTrue((create_update_imms_id, self.UPDATE_OPERATION) in delta_imms_query_response) - elif operation == self.DELETE_OPERATION: - self.assertTrue((create_delete_imms_id, self.DELETE_OPERATION) in delta_imms_query_response) diff --git a/tests/e2e/test_deployment.py b/tests/e2e/test_deployment.py deleted file mode 100644 index 3a3c759c49..0000000000 --- a/tests/e2e/test_deployment.py +++ /dev/null @@ -1,50 +0,0 @@ -import unittest -from time import sleep - -import requests -from lib.env import ( - get_service_base_path, - get_source_commit_id, - get_status_endpoint_api_key, -) - -"""Tests in this package don't really test anything. Platform created these tests to check if the current -deployment is the latest. It works by hitting /_status endpoint and comparing the commit sha code of the -deployment with the one that returned from the deployed proxy. -You can ignore these tests if you are running them in your local environment""" - - -class TestDeployment(unittest.TestCase): - proxy_url: str - status_api_key: str - expected_commit_id: str - - max_retries = 30 - - @classmethod - def setUpClass(cls): - cls.proxy_url = get_service_base_path() - cls.status_api_key = get_status_endpoint_api_key() - cls.expected_commit_id = get_source_commit_id() - - def test_wait_for_ping(self): - url = f"{self.proxy_url}/_ping" - self.check_and_retry(url, {}, self.expected_commit_id) - - def test_wait_for_status(self): - url = f"{self.proxy_url}/_status" - self.check_and_retry(url, {"apikey": self.status_api_key}, self.expected_commit_id) - - def check_and_retry(self, url, headers, expected_commit_id): - for _i in range(self.max_retries): - resp = requests.get(url, headers=headers) - status_code = resp.status_code - if status_code != 200: - self.fail(f"Status code {status_code}, expecting 200") - - deployed_commit_id = resp.json().get("commitId") - if status_code == 200 and deployed_commit_id == expected_commit_id: - return - sleep(3) - - self.fail("Timeout Error - max retries") diff --git a/tests/e2e/test_get_immunization.py b/tests/e2e/test_get_immunization.py deleted file mode 100644 index 60e8b34600..0000000000 --- a/tests/e2e/test_get_immunization.py +++ /dev/null @@ -1,83 +0,0 @@ -import uuid -from decimal import Decimal - -from utils.base_test import ImmunizationBaseTest -from utils.immunisation_api import parse_location -from utils.mappings import EndpointOperationNames, VaccineTypes -from utils.resource import generate_filtered_imms_resource, generate_imms_resource - - -class TestGetImmunization(ImmunizationBaseTest): - def test_get_imms(self): - """it should get a FHIR Immunization resource""" - for imms_api in self.imms_apis: - with self.subTest(imms_api): - # Create one shared UUID per immunization (covid & rsv) - covid_uuid = str(uuid.uuid4()) - rsv_uuid = str(uuid.uuid4()) - # Given - immunizations = [ - { - "data": generate_imms_resource(imms_identifier_value=covid_uuid), - "expected": generate_filtered_imms_resource( - crud_operation_to_filter_for=EndpointOperationNames.READ, - imms_identifier_value=covid_uuid, - ), - }, - { - "data": generate_imms_resource( - sample_data_file_name="completed_rsv_immunization_event", - vaccine_type=VaccineTypes.rsv, - imms_identifier_value=rsv_uuid, - ), - "expected": generate_filtered_imms_resource( - crud_operation_to_filter_for=EndpointOperationNames.READ, - vaccine_type=VaccineTypes.rsv, - imms_identifier_value=rsv_uuid, - ), - }, - ] - - # Create immunizations and capture IDs - for immunization in immunizations: - response = imms_api.create_immunization(immunization["data"]) - self.assertEqual(response.status_code, 201) - - immunization_id = parse_location(response.headers["Location"]) - immunization["id"] = immunization_id - immunization["expected"]["id"] = immunization_id - - # When - Retrieve and validate each immunization by ID - for immunization in immunizations: - response = imms_api.get_immunization_by_id(immunization["id"]) - # Then - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["id"], immunization["id"]) - self.assertEqual(response.json(parse_float=Decimal), immunization["expected"]) - - def not_found(self): - """it should return 404 if resource doesn't exist""" - response = self.default_imms_api.get_immunization_by_id("some-id-that-does-not-exist", expected_status_code=404) - self.assert_operation_outcome(response, 404) - - def malformed_id(self): - """it should return 400 if resource id is invalid""" - response = self.default_imms_api.get_immunization_by_id("some_id_that_is_malformed", expected_status_code=400) - self.assert_operation_outcome(response, 400) - - def get_deleted_imms(self): - """it should return 404 if resource has been deleted""" - imms = self.default_imms_api.create_a_deleted_immunization_resource() - response = self.default_imms_api.get_immunization_by_id(imms["id"], expected_status_code=404) - self.assert_operation_outcome(response, 404) - - def test_get_imms_with_tbc_pk(self): - """it should get a FHIR Immunization resource if the nhs number is TBC""" - imms = generate_imms_resource() - del imms["contained"][1]["identifier"][0]["value"] - imms_id = self.default_imms_api.create_immunization_resource(imms) - - response = self.default_imms_api.get_immunization_by_id(imms_id) - - self.assertEqual(response.status_code, 200, response.text) - self.assertEqual(response.json()["id"], imms_id) diff --git a/tests/e2e/test_proxy.py b/tests/e2e/test_proxy.py deleted file mode 100644 index ceb7874397..0000000000 --- a/tests/e2e/test_proxy.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -import subprocess -import unittest -import uuid - -import requests -from lib.env import get_service_base_path, get_status_endpoint_api_key -from utils.immunisation_api import ImmunisationApi - - -class TestProxyHealthcheck(unittest.TestCase): - proxy_url: str - status_api_key: str - - @classmethod - def setUpClass(cls): - cls.proxy_url = get_service_base_path() - cls.status_api_key = get_status_endpoint_api_key() - - def test_ping(self): - """/_ping should return 200 if proxy is up and running""" - response = ImmunisationApi.make_request_with_backoff(http_method="GET", url=f"{self.proxy_url}/_ping") - self.assertEqual(response.status_code, 200, response.text) - - def test_status(self): - """/_status should return 200 if proxy can reach to the backend""" - response = ImmunisationApi.make_request_with_backoff( - http_method="GET", - url=f"{self.proxy_url}/_status", - headers={"apikey": self.status_api_key}, - is_status_check=True, - ) - self.assertEqual(response.status_code, 200, response.text) - body = response.json() - - self.assertEqual( - body["status"].lower(), - "pass", - f"service is not healthy: status: {body['status']}", - ) - - -class TestMtls(unittest.TestCase): - """Our backend is secured using mTLS. This test makes sure you can't hit the backend directly""" - - def test_mtls(self): - """backend should reject unauthorized connections""" - backend_url = TestMtls.get_backend_url() - backend_health = f"https://{backend_url}/status" - - with self.assertRaises(requests.exceptions.RequestException) as e: - ImmunisationApi.make_request_with_backoff( - http_method="GET", - url=backend_health, - headers={"X-Request-ID": str(uuid.uuid4())}, - ) - self.assertTrue("RemoteDisconnected" in str(e.exception)) - - @staticmethod - def get_backend_url() -> str: - """The output is the backend url that terraform has deployed. - This command runs a make target in the terraform directory only if it's not in env var - """ - if url := os.getenv("AWS_DOMAIN_NAME"): - return url - - terraform_path = f"{os.getcwd()}/../../infrastructure/instance" - "make -C ../../infrastructure/instance -s output name=service_domain_name" - cmd = ["make", "-C", terraform_path, "-s", "output", "name=service_domain_name"] - try: - res = subprocess.run(cmd, stdout=subprocess.PIPE, text=True) - if res.returncode != 0: - cmd_str = " ".join(cmd) - raise RuntimeError( - f"Failed to run command: '{cmd_str}'\nDiagnostic: Try to run the same command in the " - f"same terminal. Make sure you are authenticated\n{res.stdout}" - ) - return res.stdout - except FileNotFoundError: - raise RuntimeError( - "Make sure you install terraform. This test can only be run if you have access to thebackend deployment" - ) - except RuntimeError as e: - raise RuntimeError(f"Failed to run command\n{e}") - - -class TestProxyAuthorization(unittest.TestCase): - """Our apigee proxy has its own authorization. - This class test different authorization access levels/roles authentication types that are supported - """ - - proxy_url: str - - @classmethod - def setUpClass(cls): - cls.proxy_url = get_service_base_path() - - def test_invalid_access_token(self): - """it should return 401 if access token is invalid""" - response = ImmunisationApi.make_request_with_backoff( - http_method="GET", - url=f"{self.proxy_url}/Immunization", - headers={"X-Request-ID": str(uuid.uuid4())}, - expected_status_code=401, - ) - self.assertEqual(response.status_code, 401, response.text) diff --git a/tests/e2e/test_search_by_identifier_immunization.py b/tests/e2e/test_search_by_identifier_immunization.py deleted file mode 100644 index a02492c4df..0000000000 --- a/tests/e2e/test_search_by_identifier_immunization.py +++ /dev/null @@ -1,212 +0,0 @@ -import pprint -import uuid -from decimal import Decimal -from typing import Literal, NamedTuple, Optional - -from lib.env import get_service_base_path -from utils.base_test import ImmunizationBaseTest -from utils.constants import valid_nhs_number1 -from utils.mappings import VaccineTypes -from utils.resource import generate_filtered_imms_resource, generate_imms_resource - - -class TestSearchImmunizationByIdentifier(ImmunizationBaseTest): - def store_records(self, *resources): - ids = [] - for res in resources: - imms_id = self.default_imms_api.create_immunization_resource(res) - ids.append(imms_id) - return ids[0] if len(ids) == 1 else tuple(ids) - - def test_search_imms(self): - for imms_api in self.imms_apis: - with self.subTest(imms_api): - covid_imms_data = generate_imms_resource() - rsv_imms_data = generate_imms_resource() - covid_ids = self.store_records(covid_imms_data) - rsv_ids = self.store_records(rsv_imms_data) - - # Retrieve the resources to get the identifier system and value via read API - covid_resource = imms_api.get_immunization_by_id(covid_ids).json() - rsv_resource = imms_api.get_immunization_by_id(rsv_ids).json() - - # Extract identifier components safely for covid resource - identifiers = covid_resource.get("identifier", []) - identifier_system = identifiers[0].get("system") - identifier_value = identifiers[0].get("value") - - # Extract identifier components safely for rsv resource - rsv_identifiers = rsv_resource.get("identifier", []) - rsv_identifier_system = rsv_identifiers[0].get("system") - rsv_identifier_value = rsv_identifiers[0].get("value") - - # When - search_response = imms_api.search_immunization_by_identifier(identifier_system, identifier_value) - self.assertEqual(search_response.status_code, 200, search_response.text) - bundle = search_response.json() - self.assertEqual(bundle.get("resourceType"), "Bundle", bundle) - entries = bundle.get("entry", []) - self.assertTrue(entries, "Expected at least one match in Bundle.entry") - self.assertEqual(len(entries), 1, f"Expected exactly one match, got {len(entries)}") - - # When - rsv_search_response = imms_api.search_immunization_by_identifier( - rsv_identifier_system, rsv_identifier_value - ) - self.assertEqual(rsv_search_response.status_code, 200, search_response.text) - rsv_bundle = rsv_search_response.json() - self.assertEqual(bundle.get("resourceType"), "Bundle", rsv_bundle) - entries = rsv_bundle.get("entry", []) - self.assertTrue(entries, "Expected at least one match in Bundle.entry") - self.assertEqual(len(entries), 1, f"Expected exactly one match, got {len(entries)}") - - def test_search_backwards_compatible(self): - """Test that SEARCH 200 response body is backwards compatible with Immunisation History FHIR API. - This test proves that the search endpoint’s response is still shaped exactly like the - Immunisation History FHIR API expects (“backwards compatible”), not just that it returns a 200 - """ - for imms_api in self.imms_apis: - with self.subTest(imms_api): - stored_imms_resource = generate_imms_resource() - imms_identifier_value = stored_imms_resource["identifier"][0]["value"] - imms_id = self.store_records(stored_imms_resource) - - # Prepare the imms resource expected from the response. Note that id and identifier_value need to be - # updated to match those assigned by the create_an_imms_obj and store_records functions. - expected_imms_resource = generate_filtered_imms_resource( - crud_operation_to_filter_for="SEARCH", - imms_identifier_value=imms_identifier_value, - nhs_number=valid_nhs_number1, - vaccine_type=VaccineTypes.covid, - ) - expected_imms_resource["id"] = imms_id - expected_imms_resource["meta"] = {"versionId": "1"} - - # Retrieve the resource to get the identifier system and value via READ API - imms_resource = imms_api.get_immunization_by_id(imms_id).json() - identifiers = imms_resource.get("identifier", []) - identifier_system = identifiers[0].get("system") - identifier_value = identifiers[0].get("value") - self.assertIsNotNone(identifier_system, "Identifier system is None") - self.assertIsNotNone(identifier_value, "Identifier value is None") - - # When - response = imms_api.search_immunization_by_identifier(identifier_system, identifier_value) - - # Then - self.assertEqual(response.status_code, 200, response.text) - body = response.json(parse_float=Decimal) - entries = body["entry"] - response_imms = [item for item in entries if item["resource"]["resourceType"] == "Immunization"] - response_patients = [item for item in entries if item["resource"]["resourceType"] == "Patient"] - response_other_entries = [ - item for item in entries if item["resource"]["resourceType"] not in ("Patient", "Immunization") - ] - - # Check bundle structure apart from entry - self.assertEqual(body["resourceType"], "Bundle") - self.assertEqual(body["type"], "searchset") - self.assertEqual(body["total"], len(response_imms)) - - # Check that entry only contains a patient and immunizations - self.assertEqual(len(response_other_entries), 0) - self.assertEqual(len(response_patients), 0) - - # Check Immunization structure - for entry in response_imms: - self.assertEqual(entry["search"], {"mode": "match"}) - self.assertTrue(entry["fullUrl"].startswith("https://")) - self.assertEqual(entry["resource"]["resourceType"], "Immunization") - imms_identifier = entry["resource"]["identifier"] - self.assertEqual( - len(imms_identifier), - 1, - "Immunization did not have exactly 1 identifier", - ) - self.assertEqual(imms_identifier[0]["system"], identifier_system) - self.assertEqual(imms_identifier[0]["value"], identifier_value) - - # Check structure of one of the imms resources - response_imm = next(item for item in entries if item["resource"]["id"] == imms_id) - self.assertEqual( - response_imm["fullUrl"], - f"{get_service_base_path()}/Immunization/{imms_id}", - ) - self.assertEqual(response_imm["search"], {"mode": "match"}) - expected_imms_resource["patient"]["reference"] = response_imm["resource"]["patient"]["reference"] - self.assertEqual(response_imm["resource"], expected_imms_resource) - - def test_search_immunization_parameter_smoke_tests(self): - stored_records = generate_imms_resource( - valid_nhs_number1, - VaccineTypes.covid, - imms_identifier_value=str(uuid.uuid4()), - ) - - imms_id = self.store_records(stored_records) - # Retrieve the resources to get the identifier system and value via read API - covid_resource = self.default_imms_api.get_immunization_by_id(imms_id).json() - - # Extract identifier components safely for covid resource - identifiers = covid_resource.get("identifier", []) - identifier_system = identifiers[0].get("system") - identifier_value = identifiers[0].get("value") - - # created_resource_ids = [result["id"] for result in stored_records] - - class SearchTestParams(NamedTuple): - method: Literal["POST", "GET"] - query_string: Optional[str] - body: Optional[str] - should_be_success: bool - expected_status_code: int = 200 - - searches = [ - SearchTestParams("GET", "", None, False, 400), - # No results. - SearchTestParams( - "GET", - f"identifier={identifier_system}|{identifier_value}", - None, - True, - 200, - ), - SearchTestParams( - "POST", - "", - f"identifier={identifier_system}|{identifier_value}", - True, - 200, - ), - SearchTestParams( - "POST", - f"identifier={identifier_system}|{identifier_value}", - f"identifier={identifier_system}|{identifier_value}", - True, - 200, - ), - ] - for search in searches: - pprint.pprint(search) - response = self.default_imms_api.search_immunizations_full( - search.method, - search.query_string, - body=search.body, - expected_status_code=search.expected_status_code, - ) - - # Then - assert response.ok == search.should_be_success, response.text - - results: dict = response.json() - if search.should_be_success: - assert "entry" in results.keys() - assert response.status_code == 200 - assert results["resourceType"] == "Bundle" - assert results["type"] == "searchset" - assert results["total"] == 1 - assert isinstance(results["entry"], list) - else: - assert "entry" not in results.keys() - assert response.status_code != 200 - assert results["resourceType"] == "OperationOutcome" diff --git a/tests/e2e/test_search_identifier_elements_immunization.py b/tests/e2e/test_search_identifier_elements_immunization.py deleted file mode 100644 index 5431c6931d..0000000000 --- a/tests/e2e/test_search_identifier_elements_immunization.py +++ /dev/null @@ -1,139 +0,0 @@ -import pprint -import uuid -from typing import Literal, NamedTuple, Optional - -from lib.env import get_service_base_path -from utils.base_test import ImmunizationBaseTest -from utils.constants import valid_nhs_number1 -from utils.mappings import VaccineTypes -from utils.resource import generate_imms_resource - - -class TestSearchImmunizationByIdentifier(ImmunizationBaseTest): - def store_records(self, *resources): - ids = [] - for res in resources: - imms_id = self.default_imms_api.create_immunization_resource(res) - ids.append(imms_id) - return ids[0] if len(ids) == 1 else tuple(ids) - - def test_search_imms(self): - for imms_api in self.imms_apis: - with self.subTest(imms_api): - covid_imms_data = generate_imms_resource() - covid_ids = self.store_records(covid_imms_data) - - # Retrieve the resources to get the identifier system and value via read API - covid_resource = imms_api.get_immunization_by_id(covid_ids).json() - - # Extract identifier components safely for covid resource - identifiers = covid_resource.get("identifier", []) - identifier_system = identifiers[0].get("system") - identifier_value = identifiers[0].get("value") - - # When - search_response = imms_api.search_immunization_by_identifier_and_elements( - identifier_system, identifier_value - ) - self.assertEqual(search_response.status_code, 200, search_response.text) - bundle = search_response.json() - self.assertEqual(bundle.get("resourceType"), "Bundle", bundle) - entries = bundle.get("entry", []) - self.assertTrue(entries, "Expected at least one match in Bundle.entry") - self.assertEqual(len(entries), 1, f"Expected exactly one match, got {len(entries)}") - self.assertIn("meta", entries[0]["resource"]) - self.assertEqual(entries[0]["resource"]["id"], covid_ids) - self.assertEqual(entries[0]["resource"]["meta"]["versionId"], 1) - self.assertTrue(entries[0]["fullUrl"].startswith("https://")) - self.assertEqual( - entries[0]["fullUrl"], - f"{get_service_base_path()}/Immunization/{covid_ids}", - ) - - def test_search_imms_no_match_returns_empty_bundle(self): - for imms_api in self.imms_apis: - with self.subTest(imms_api): - resp = imms_api.search_immunization_by_identifier_and_elements( - "http://example.org/sys", "does-not-exist-123" - ) - self.assertEqual(resp.status_code, 200, resp.text) - bundle = resp.json() - self.assertEqual(bundle.get("resourceType"), "Bundle", bundle) - self.assertEqual(bundle.get("type"), "searchset") - self.assertEqual(bundle.get("total", 0), 0) - self.assertFalse(bundle.get("entry")) - - def test_search_by_identifier_parameter_smoke_tests(self): - stored_records = generate_imms_resource( - valid_nhs_number1, - VaccineTypes.covid, - imms_identifier_value=str(uuid.uuid4()), - ) - - imms_id = self.store_records(stored_records) - # Retrieve the resources to get the identifier system and value via read API - covid_resource = self.default_imms_api.get_immunization_by_id(imms_id).json() - - # Extract identifier components safely for covid resource - identifiers = covid_resource.get("identifier", []) - identifier_system = identifiers[0].get("system") - identifier_value = identifiers[0].get("value") - - # created_resource_ids = [result["id"] for result in stored_records] - - class SearchTestParams(NamedTuple): - method: Literal["POST", "GET"] - query_string: Optional[str] - body: Optional[str] - should_be_success: bool - expected_status_code: int = 200 - - searches = [ - SearchTestParams("GET", "", None, False, 400), - # No results. - SearchTestParams( - "GET", - f"identifier={identifier_system}|{identifier_value}", - None, - True, - 200, - ), - SearchTestParams( - "POST", - "", - f"identifier={identifier_system}|{identifier_value}", - True, - 200, - ), - SearchTestParams( - "POST", - f"identifier={identifier_system}|{identifier_value}", - f"identifier={identifier_system}|{identifier_value}", - True, - 200, - ), - ] - for search in searches: - pprint.pprint(search) - response = self.default_imms_api.search_immunizations_full( - search.method, - search.query_string, - body=search.body, - expected_status_code=search.expected_status_code, - ) - - # Then - assert response.ok == search.should_be_success, response.text - - results: dict = response.json() - if search.should_be_success: - assert "entry" in results.keys() - assert response.status_code == 200 - assert results["resourceType"] == "Bundle" - assert results["type"] == "searchset" - assert results["total"] == 1 - assert isinstance(results["entry"], list) - else: - assert "entry" not in results.keys() - assert response.status_code != 200 - assert results["resourceType"] == "OperationOutcome" diff --git a/tests/e2e/test_search_immunization.py b/tests/e2e/test_search_immunization.py deleted file mode 100644 index 8464653d23..0000000000 --- a/tests/e2e/test_search_immunization.py +++ /dev/null @@ -1,440 +0,0 @@ -import pprint -import uuid -from decimal import Decimal -from typing import List, Literal, NamedTuple, Optional - -from lib.env import get_service_base_path -from utils.base_test import ImmunizationBaseTest -from utils.constants import ( - valid_nhs_number1, - valid_nhs_number2, - valid_patient_identifier1, - valid_patient_identifier2, -) -from utils.mappings import VaccineTypes -from utils.resource import generate_filtered_imms_resource, generate_imms_resource - - -class TestSearchImmunization(ImmunizationBaseTest): - # NOTE: In each test, the result may contain more hits. We only assert if the resource that we created is - # in the result set and assert the one that we don't expect is not present. - # This is to make these tests stateless otherwise; we need to clean up the db after each test - - def store_records(self, *resources): - ids = [] - for res in resources: - imms_id = self.default_imms_api.create_immunization_resource(res) - ids.append(imms_id) - return ids[0] if len(ids) == 1 else tuple(ids) - - def test_search_imms(self): - """it should search records given nhs-number and vaccine type""" - for imms_api in self.imms_apis: - with self.subTest(imms_api): - # Given two patients each with one covid - covid_p1 = generate_imms_resource(valid_nhs_number1, VaccineTypes.covid) - covid_p2 = generate_imms_resource(valid_nhs_number2, VaccineTypes.covid) - rsv_p1 = generate_imms_resource(valid_nhs_number1, VaccineTypes.rsv) - rsv_p2 = generate_imms_resource(valid_nhs_number2, VaccineTypes.rsv) - covid_p1_id, covid_p2_id = self.store_records(covid_p1, covid_p2) - rsv_p1_id, rsv_p2_id = self.store_records(rsv_p1, rsv_p2) - - # When - response = imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.covid) - response_rsv = imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.rsv) - - # Then - self.assertEqual(response.status_code, 200, response.text) - body = response.json() - self.assertEqual(body["resourceType"], "Bundle") - - resource_ids = [entity["resource"]["id"] for entity in body["entry"]] - self.assertTrue(covid_p1_id in resource_ids) - self.assertTrue(covid_p2_id not in resource_ids) - - self.assertEqual(response_rsv.status_code, 200, response_rsv.text) - body_rsv = response_rsv.json() - self.assertEqual(body_rsv["resourceType"], "Bundle") - - resource_ids = [entity["resource"]["id"] for entity in body_rsv["entry"]] - self.assertTrue(rsv_p1_id in resource_ids) - self.assertTrue(rsv_p2_id not in resource_ids) - - def test_search_patient_multiple_diseases(self): - # Given patient has two vaccines - covid = generate_imms_resource(valid_nhs_number1, VaccineTypes.covid) - flu = generate_imms_resource(valid_nhs_number1, VaccineTypes.flu) - covid_id, flu_id = self.store_records(covid, flu) - - # When - response = self.default_imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.covid) - - # Then - self.assertEqual(response.status_code, 200, response.text) - body = response.json() - - resource_ids = [entity["resource"]["id"] for entity in body["entry"]] - self.assertIn(covid_id, resource_ids) - self.assertNotIn(flu_id, resource_ids) - - def test_search_backwards_compatible(self): - """Test that SEARCH 200 response body is backwards compatible with Immunisation History FHIR API""" - for imms_api in self.imms_apis: - with self.subTest(imms_api): - # Given that the patient has a covid vaccine event stored in the IEDS - stored_imms_resource = generate_imms_resource(valid_nhs_number1, VaccineTypes.covid) - imms_identifier_value = stored_imms_resource["identifier"][0]["value"] - imms_id = self.store_records(stored_imms_resource) - - # Prepare the imms resource expected from the response. Note that id and identifier_value need to be - # updated to match those assigned by the create_an_imms_obj and store_records functions. - expected_imms_resource = generate_filtered_imms_resource( - crud_operation_to_filter_for="SEARCH", - imms_identifier_value=imms_identifier_value, - nhs_number=valid_nhs_number1, - vaccine_type=VaccineTypes.covid, - ) - expected_imms_resource["id"] = imms_id - expected_imms_resource["meta"] = {"versionId": "1"} - - # When - response = imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.covid) - - # Then - self.assertEqual(response.status_code, 200, response.text) - body = response.json(parse_float=Decimal) - entries = body["entry"] - response_imms = [item for item in entries if item["resource"]["resourceType"] == "Immunization"] - response_patients = [item for item in entries if item["resource"]["resourceType"] == "Patient"] - response_other_entries = [ - item for item in entries if item["resource"]["resourceType"] not in ("Patient", "Immunization") - ] - - # Check bundle structure apart from entry - self.assertEqual(body["resourceType"], "Bundle") - self.assertEqual(body["type"], "searchset") - self.assertEqual(body["total"], len(response_imms)) - - # Check that entry only contains a patient and immunizations - self.assertEqual(len(response_other_entries), 0) - self.assertEqual(len(response_patients), 1) - - # Check patient structure - response_patient = response_patients[0] - self.assertEqual(response_patient["search"], {"mode": "include"}) - self.assertTrue(response_patient["fullUrl"].startswith("urn:uuid:")) - self.assertTrue(uuid.UUID(response_patient["fullUrl"].split(":")[2])) - expected_patient_resource_keys = ["resourceType", "id", "identifier"] - self.assertEqual( - sorted(response_patient["resource"].keys()), - sorted(expected_patient_resource_keys), - ) - self.assertEqual(response_patient["resource"]["id"], valid_nhs_number1) - patient_identifier = response_patient["resource"]["identifier"] - # NOTE: If PDS response ever changes to send more than one identifier then the below will break - self.assertEqual(len(patient_identifier), 1, "PDS did not return 1 identifier") - self.assertEqual(sorted(patient_identifier[0].keys()), sorted(["system", "value"])) - self.assertEqual(patient_identifier[0]["system"], "https://fhir.nhs.uk/Id/nhs-number") - self.assertEqual(patient_identifier[0]["value"], valid_nhs_number1) - - # Check structure of one of the imms resources - expected_imms_resource["patient"]["reference"] = response_patient["fullUrl"] - response_imm = next(item for item in entries if item["resource"]["id"] == imms_id) - self.assertEqual( - response_imm["fullUrl"], - f"{get_service_base_path()}/Immunization/{imms_id}", - ) - self.assertEqual(response_imm["search"], {"mode": "match"}) - self.assertEqual(response_imm["resource"], expected_imms_resource) - - def test_search_ignore_deleted(self): - # Given patient has three vaccines and the last one is deleted - mmr1 = generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr) - mmr2 = generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr) - mmr1_id, mmr2_id = self.store_records(mmr1, mmr2) - - to_delete_mmr = generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr) - deleted_mmr = self.default_imms_api.create_a_deleted_immunization_resource(to_delete_mmr) - - # When - response = self.default_imms_api.search_immunizations(valid_nhs_number1, VaccineTypes.mmr) - - # Then - self.assertEqual(response.status_code, 200, response.text) - body = response.json() - - resource_ids = [entity["resource"]["id"] for entity in body["entry"]] - self.assertTrue(mmr1_id in resource_ids) - self.assertTrue(mmr2_id in resource_ids) - self.assertTrue(deleted_mmr["id"] not in resource_ids) - - def test_search_immunization_parameter_smoke_tests(self): - time_1 = "2024-01-30T13:28:17.271+00:00" - time_2 = "2024-02-01T13:28:17.271+00:00" - stored_records = [ - generate_imms_resource( - valid_nhs_number1, - VaccineTypes.mmr, - imms_identifier_value=str(uuid.uuid4()), - ), - generate_imms_resource( - valid_nhs_number1, - VaccineTypes.flu, - imms_identifier_value=str(uuid.uuid4()), - ), - generate_imms_resource( - valid_nhs_number1, - VaccineTypes.covid, - imms_identifier_value=str(uuid.uuid4()), - ), - generate_imms_resource( - valid_nhs_number1, - VaccineTypes.covid, - occurrence_date_time=time_1, - imms_identifier_value=str(uuid.uuid4()), - ), - generate_imms_resource( - valid_nhs_number1, - VaccineTypes.covid, - occurrence_date_time=time_2, - imms_identifier_value=str(uuid.uuid4()), - ), - generate_imms_resource( - valid_nhs_number2, - VaccineTypes.flu, - imms_identifier_value=str(uuid.uuid4()), - ), - generate_imms_resource( - valid_nhs_number2, - VaccineTypes.covid, - imms_identifier_value=str(uuid.uuid4()), - ), - ] - - created_resource_ids = list(self.store_records(*stored_records)) - # created_resource_ids = [result["id"] for result in stored_records] - - # When - class SearchTestParams(NamedTuple): - method: Literal["POST", "GET"] - query_string: Optional[str] - body: Optional[str] - should_be_success: bool - expected_indexes: List[int] - expected_status_code: int = 200 - - searches = [ - SearchTestParams("GET", "", None, False, [], 400), - # No results. - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier2}&-immunization.target={VaccineTypes.mmr}", - None, - True, - [], - 200, - ), - # Basic success. - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}", - None, - True, - [0], - 200, - ), - # "Or" params. - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}," - + f"{VaccineTypes.flu}", - None, - True, - [0, 1], - 200, - ), - # GET does not support body. - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}", - f"patient.identifier={valid_patient_identifier1}", - True, - [0], - 200, - ), - SearchTestParams( - "POST", - None, - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}", - True, - [0], - 200, - ), - # Duplicated NHS number not allowed, spread across query and content. - SearchTestParams( - "POST", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}", - f"patient.identifier={valid_patient_identifier1}", - False, - [], - 400, - ), - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}" - f"&patient.identifier={valid_patient_identifier1}" - f"&-immunization.target={VaccineTypes.mmr}", - None, - False, - [], - 400, - ), - # "And" params not supported. - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}" - f"&-immunization.target={VaccineTypes.flu}", - None, - False, - [], - 400, - ), - # Date - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.covid}", - None, - True, - [2, 3, 4], - 200, - ), - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.covid}" - f"&-date.from=2024-01-30", - None, - True, - [3, 4], - 200, - ), - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.covid}" - f"&-date.to=2024-01-30", - None, - True, - [2, 3], - 200, - ), - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.covid}" - f"&-date.from=2024-01-01&-date.to=2024-01-30", - None, - True, - [3], - 200, - ), - # "from" after "to" is an error. - SearchTestParams( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.covid}" - f"&-date.from=2024-02-01&-date.to=2024-01-30", - None, - False, - [0], - 400, - ), - ] - - for search in searches: - pprint.pprint(search) - response = self.default_imms_api.search_immunizations_full( - search.method, - search.query_string, - body=search.body, - expected_status_code=search.expected_status_code, - ) - - # Then - assert response.ok == search.should_be_success, response.text - - results: dict = response.json() - if search.should_be_success: - assert "entry" in results.keys() - assert response.status_code == 200 - assert results["resourceType"] == "Bundle" - - result_ids = [result["resource"]["id"] for result in results["entry"]] - created_and_returned_ids = list(set(result_ids) & set(created_resource_ids)) - assert len(created_and_returned_ids) == len(search.expected_indexes) - for expected_index in search.expected_indexes: - assert created_resource_ids[expected_index] in result_ids - - def test_search_immunization_accepts_include_and_provides_patient(self): - """it should accept the _include parameter of "Immunization:patient" and return the patient.""" - - # Arrange - imms_obj = generate_imms_resource(valid_nhs_number1, VaccineTypes.mmr) - imms_obj_id = self.store_records(imms_obj) - - response = self.default_imms_api.search_immunizations_full( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}" - + "&_include=Immunization:patient", - body=None, - ) - - assert response.ok - result = response.json() - entries = result["entry"] - - entry_ids = [result["resource"]["id"] for result in result["entry"]] - assert imms_obj_id in entry_ids - - patient_entry = next(entry for entry in entries if entry["resource"]["resourceType"] == "Patient") - assert patient_entry["search"]["mode"] == "include" - - assert patient_entry["resource"]["identifier"][0]["system"] == "https://fhir.nhs.uk/Id/nhs-number" - assert patient_entry["resource"]["identifier"][0]["value"] == valid_nhs_number1 - - response_without_include = self.default_imms_api.search_immunizations_full( - "GET", - f"patient.identifier={valid_patient_identifier1}&-immunization.target={VaccineTypes.mmr}", - body=None, - ) - - assert response_without_include.ok - result_without_include = response_without_include.json() - - # Matches Immunisation History API in that it doesn't matter if you don't pass "_include". - - # Ignore link, patient full url and immunisation patient reference as these will always differ. - result["link"] = [] - result_without_include["link"] = [] - - for entry in result["entry"]: - if entry["resource"]["resourceType"] == "Immunization": - entry["resource"]["patient"]["reference"] = "MOCK VALUE" - elif entry["resource"]["resourceType"] == "Patient": - entry["fullUrl"] = "MOCK VALUE" - - for entry in result_without_include["entry"]: - if entry["resource"]["resourceType"] == "Immunization": - entry["resource"]["patient"]["reference"] = "MOCK VALUE" - elif entry["resource"]["resourceType"] == "Patient": - entry["fullUrl"] = "MOCK VALUE" - - self.assertEqual(result, result_without_include) - - def test_search_reject_tbc(self): - # Given patient has a vaccine with no NHS number - imms = generate_imms_resource("TBC", VaccineTypes.mmr) - del imms["contained"][1]["identifier"][0]["value"] - self.store_records(imms) - - # When - response = self.default_imms_api.search_immunizations("TBC", f"{VaccineTypes.mmr}", expected_status_code=400) - - # Then - self.assert_operation_outcome(response, 400) diff --git a/tests/e2e/test_sqs_dlq.py b/tests/e2e/test_sqs_dlq.py deleted file mode 100644 index d92579e6cf..0000000000 --- a/tests/e2e/test_sqs_dlq.py +++ /dev/null @@ -1,34 +0,0 @@ -import json -import os -import unittest - -import boto3 -from botocore.exceptions import ClientError # Handle potential errors -from utils.delete_sqs_messages import read_and_delete_messages -from utils.get_sqs_url import get_queue_url - - -class TestSQS(unittest.TestCase): - def setUp(self): - # Get SQS queue url - self.queue_name = os.environ["AWS_SQS_QUEUE_NAME"] - self.queue_url = get_queue_url(self.queue_name) - read_and_delete_messages(self.queue_url) - - def test_send_message(self): - # Create a message - message_body = {"message": "This is a test message"} - # Use boto3 to interact with SQS - sqs_client = boto3.client("sqs") - try: - # Send the message to the queue - response = sqs_client.send_message(QueueUrl=self.queue_url, MessageBody=json.dumps(message_body)) - read_and_delete_messages(self.queue_url) - # Assert successful message sending - self.assertIn("MessageId", response) - except ClientError as e: - self.fail(f"Error sending message to SQS: {e}") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/e2e/test_update_immunization.py b/tests/e2e/test_update_immunization.py deleted file mode 100644 index 451184a0f4..0000000000 --- a/tests/e2e/test_update_immunization.py +++ /dev/null @@ -1,75 +0,0 @@ -import copy -import uuid - -from utils.base_test import ImmunizationBaseTest -from utils.immunisation_api import parse_location -from utils.resource import generate_imms_resource - - -class TestUpdateImmunization(ImmunizationBaseTest): - def test_update_imms(self): - """it should update a FHIR Immunization resource""" - for imms_api in self.imms_apis: - with self.subTest(imms_api): - # Given - immunization_resources = [ - generate_imms_resource(), - generate_imms_resource(sample_data_file_name="completed_rsv_immunization_event"), - ] - - for imms in immunization_resources: - # Create the immunization resource - response = imms_api.create_immunization(imms) - assert response.status_code == 201 - imms_id = parse_location(response.headers["Location"]) - - # When - update_payload = copy.deepcopy(imms) - update_payload["id"] = imms_id - update_payload["location"]["identifier"]["value"] = "Y11111" - response = imms_api.update_immunization(imms_id, update_payload) - - # Then - self.assertEqual(response.status_code, 200, response.text) - self.assertEqual(int(response.headers["E-Tag"]), 2) - self.assertNotIn("Location", response.headers) - - def test_update_non_existent_identifier(self): - """update a record should fail if identifier is not present""" - imms = generate_imms_resource() - _ = self.default_imms_api.create_immunization_resource(imms) - # NOTE: there is a difference between id and identifier. - # 422 is expected when identifier is the same across different ids - # This is why in this test we create a new id but not touching the identifier - new_imms_id = str(uuid.uuid4()) - imms["id"] = new_imms_id - - # When update the same object (it has the same identifier) - response = self.default_imms_api.update_immunization(new_imms_id, imms, expected_status_code=404) - # Then - self.assert_operation_outcome(response, 404) - - def test_update_inconsistent_id(self): - """update should fail if id in the path doesn't match with the id in the message""" - msg_id = str(uuid.uuid4()) - imms = generate_imms_resource() - imms["id"] = msg_id - path_id = str(uuid.uuid4()) - response = self.default_imms_api.update_immunization(path_id, imms, expected_status_code=400) - self.assert_operation_outcome(response, 400, contains=path_id) - - # TODO: Uncomment this test if it is needed - # def test_update_deleted_imms(self): - # """updating deleted record will undo the delete""" - # # This behaviour is consistent. Getting a deleted record will result in a 404. - # # An update of a non-existent record should result in creating a new record - # # Therefore, the new resource's id must be different from the original one - - # imms = self.create_a_deleted_immunization_resource(self.default_imms_api) - # deleted_id = imms["id"] - - # response = self.default_imms_api.update_immunization(deleted_id, imms) - - # self.assertEqual(response.status_code, 201, response.text) - # new_imms_id = parse_location(response.headers["Location"]) - # self.assertNotEqual(deleted_id, new_imms_id) diff --git a/tests/e2e/utils/authorization.py b/tests/e2e/utils/authorization.py deleted file mode 100644 index 48b8370ab5..0000000000 --- a/tests/e2e/utils/authorization.py +++ /dev/null @@ -1,25 +0,0 @@ -from enum import Enum -from typing import Set - - -class Permission(str, Enum): - READ = "immunization:read" - CREATE = "immunization:create" - UPDATE = "immunization:update" - DELETE = "immunization:delete" - SEARCH = "immunization:search" - - -def make_permissions_attribute(permissions: Set[Permission]) -> (str, str): - """It generates an attribute value for an application restricted app. It returns key and value""" - return "Permissions", ",".join(permissions) - - -def make_vaxx_permissions_attribute(vaxx_permissions: Set) -> (str, str): - """It generates an attribute value for Vaccine Type Permissions. It returns key and value""" - return "VaccineTypePermissions", ",".join(vaxx_permissions) - - -def app_full_access(exclude: Set[Permission] = None) -> Set[Permission]: - exclude = exclude if exclude else {} - return {*Permission}.difference(exclude) diff --git a/tests/e2e/utils/base_test.py b/tests/e2e/utils/base_test.py deleted file mode 100644 index 9f648b3937..0000000000 --- a/tests/e2e/utils/base_test.py +++ /dev/null @@ -1,111 +0,0 @@ -import unittest -import uuid -from typing import List - -from lib.apigee import ApigeeApp, ApigeeProduct, ApigeeService -from lib.authentication import ( - AppRestrictedAuthentication, - Cis2Authentication, - LoginUser, -) -from lib.env import ( - get_auth_url, - get_proxy_name, - get_service_base_path, -) -from utils.constants import cis2_user -from utils.factories import ( - make_apigee_product, - make_apigee_service, - make_app_restricted_app, - make_cis2_app, -) -from utils.immunisation_api import ImmunisationApi - - -class ImmunizationBaseTest(unittest.TestCase): - """It provides a set of apps with for each AuthType with full permission""" - - apigee_service: ApigeeService - product: ApigeeProduct - - # a list of ImmunizationApi for each authentication type - imms_apis: List[ImmunisationApi] - # a list of all apps that was created, so we can delete them at the end - apps: List[ApigeeApp] - # an ImmunisationApi with default auth-type: ApplicationRestricted - default_imms_api: ImmunisationApi - - # Called once before any test methods in the class are run. - # The purpose of setUpClass is to prepare shared resources that all tests in the class can use - @classmethod - def setUpClass(cls): - cls.apps = [] - cls.imms_apis = [] - cls.apigee_service = make_apigee_service() - base_url = get_service_base_path() - try: - display_name = f"test-{get_proxy_name()}" - product_data = ApigeeProduct( - name=str(uuid.uuid4()), - displayName=display_name, - # we only use one single product for all auth types - # TODO(Cis2_AMB-1733) add scopes for Cis2 - # TODO(NhsLogin_AMB-1923) add scopes for NhsLogin - scopes=["urn:nhsd:apim:app:level3:immunisation-fhir-api"], - ) - cls.product = make_apigee_product(cls.apigee_service, product_data) - cls.apigee_service.add_proxy_to_product(product_name=cls.product.name, proxy_name=get_proxy_name()) - - def make_app_data() -> ApigeeApp: - _app = ApigeeApp(name=str(uuid.uuid4())) - _app.set_display_name(display_name) - _app.add_product(cls.product.name) - return _app - - # ApplicationRestricted - app_data = make_app_data() - app_res_app, app_res_cfg = make_app_restricted_app(cls.apigee_service, app_data) - cls.apps.append(app_res_app) - - app_res_auth = AppRestrictedAuthentication(get_auth_url(), app_res_cfg) - - cls.default_imms_api = ImmunisationApi(base_url, app_res_auth) - cls.imms_apis.append(cls.default_imms_api) - - # Cis2 - app_data = make_app_data() - cis2_app, app_res_cfg = make_cis2_app(cls.apigee_service, app_data) - cls.apps.append(cis2_app) - - cis2_auth = Cis2Authentication(get_auth_url(), app_res_cfg, LoginUser(username=cis2_user)) - cis2_imms_api = ImmunisationApi(base_url, cis2_auth) - cls.imms_apis.append(cis2_imms_api) - - # NhsLogin - # TODO(NhsLogin_AMB-1923) create an app for NhsLogin and append it to the cls.apps, - # then create ImmunisationApi and append it to cls.imms_apis - except Exception as e: - cls.tearDownClass() - raise e - - # Class method that runs once after all test methods in the class have finished. - # It is used to clean up resources that were shared across multiple tests - @classmethod - def tearDownClass(cls): - for app in cls.apps: - cls.apigee_service.delete_application(app.name) - if hasattr(cls, "product") and cls: - cls.apigee_service.delete_product(cls.product.name) - - # Runs after each individual test method in a test class. - # It’s used to clean up resources that were initialized specifically for a single test. - def tearDown(cls): - for api_client in cls.imms_apis: - api_client.cleanup_test_records() - - def assert_operation_outcome(self, response, status_code: int, contains: str = ""): - body = response.json() - self.assertEqual(response.status_code, status_code, response.text) - self.assertEqual(body["resourceType"], "OperationOutcome") - self.assertTrue(contains in body["issue"][0]["diagnostics"]) diff --git a/tests/e2e/utils/batch.py b/tests/e2e/utils/batch.py deleted file mode 100644 index 5f22aae404..0000000000 --- a/tests/e2e/utils/batch.py +++ /dev/null @@ -1,256 +0,0 @@ -import io -import os -import shutil -import subprocess -from dataclasses import dataclass -from typing import List, OrderedDict, Tuple - -"""Every thing you need to create a batch file and upload it to s3""" - -# example headers for the batch file -base_headers = [ - "NHS_NUMBER", - "PERSON_FORENAME", - "PERSON_SURNAME", - "PERSON_DOB", - "PERSON_GENDER_CODE", - "PERSON_POSTCODE", - "DATE_AND_TIME", - "SITE_CODE", - "UNIQUE_ID", - "UNIQUE_ID_URI", - "ACTION_FLAG", - "PERFORMING_PROFESSIONAL_FORENAME", - "PERFORMING_PROFESSIONAL_SURNAME", - "PERFORMING_PROFESSIONAL_BODY_REG_CODE", - "PERFORMING_PROFESSIONAL_BODY_REG_URI", - "SDS_JOB_ROLE_NAME", - "RECORDED_DATE", - "PRIMARY_SOURCE", - "REPORT_ORIGIN", - "VACCINATION_PROCEDURE_CODE", - "VACCINATION_PROCEDURE_TERM", - "VACCINATION_SITUATION_CODE", - "VACCINATION_SITUATION_TERM", - "NOT_GIVEN", - "REASON_NOT_GIVEN_CODE", - "REASON_NOT_GIVEN_TERM", - "DOSE_SEQUENCE", - "VACCINE_PRODUCT_CODE", - "VACCINE_PRODUCT_TERM", - "VACCINE_MANUFACTURER", - "BATCH_NUMBER", - "EXPIRY_DATE", - "SITE_OF_VACCINATION_CODE", - "SITE_OF_VACCINATION_TERM", - "ROUTE_OF_VACCINATION_CODE", - "ROUTE_OF_VACCINATION_TERM", - "DOSE_AMOUNT", - "DOSE_UNIT_CODE", - "DOSE_UNIT_TERM", - "INDICATION_CODE", - "INDICATION_TERM", - "NHS_NUMBER_STATUS_INDICATOR_CODE", - "NHS_NUMBER_STATUS_INDICATOR_DESCRIPTION", - "SITE_CODE_TYPE_URI", - "LOCAL_PATIENT_ID", - "LOCAL_PATIENT_URI", - "CONSENT_FOR_TREATMENT_CODE", - "CONSENT_FOR_TREATMENT_DESCRIPTION", - "CARE_SETTING_TYPE_CODE", - "CARE_SETTING_TYPE_DESCRIPTION", - "IP_ADDRESS", - "USER_ID", - "USER_NAME", - "USER_EMAIL", - "SUBMITTED_TIMESTAMP", - "LOCATION_CODE", - "LOCATION_CODE_TYPE_URI", -] - -# example to populate a base row with -base_value = [ - "9726811104", - "HELENA", - "GANDY", - "19730409", - "9", - "RG1 7YE", - "20240221T141951", - "RVVKC", - "001_COVID_4_1_Positive_First_Dose_20240222164519", - "https://evahealth.co.uk/identifier/vacc", - "new", - "Ellena", - "O'Reilly", - "2038243", - "https://fhir.hl7.org.uk/Id/gphc-number", - "DEFAULT_JOB_ROLE_NAME", - "20240221", - "TRUE", - "X99999", - "1324681000000101", - "Administration of first dose of SARS-CoV-2 (severe acute respiratory syndrome coronavirus 2) vaccine", - "", - "", - "FALSE", - "", - "", - 1, - "39326911000001101", - "Spikevax COVID-19 mRNA Vaccine 0.1mg/0.5mL dose dispersion for injection multidose vials (Moderna, Inc)", - "Moderna", - "BN1231231AW", - "20240303", - "368208006", - "Left upper arm structure (body structure)", - "78421000", - "Intramuscular route (qualifier value)", - "0.5", - "258773002", - "Milliliter (qualifier value)", - "443684005", - "Disease Outbreak (event)", - "02", - "Number present but not traced", - "https://fhir.nhs.uk/Id/ods-organization-code", - "528458", - "https://nivs.ardengemcsu.nhs.uk_COVID", - "762911000000102", - "Informed consent given for treatment (finding)", - "413294000", - "Community health services (qualifier value)", - "192.168.0.56", - "634165", - "bloggsj21", - "joebloggs21@test.net", - "20240221T17193000", - "RJC02", - "https://fhir.nhs.uk/Id/ods-organization-code", -] - -base_record = OrderedDict[str, str](zip(base_headers, base_value)) - - -def _make_header(stream: io.BytesIO, headers: List[str]): - headers_str = "|".join(headers) - stream.write(headers_str.encode()) - stream.write(b"\n") - - -def _make_row_entry(record: OrderedDict[str, str], stream: io.BytesIO): - row_str = "|".join(f'"{v}"' for v in record.values()) - row_str += "\n" - stream.write(row_str.encode()) - - -def _get_terraform_output(output_name: str) -> str: - # NOTE: We need to run make target to get output values from our deployment - terraform_dir = f"{os.path.abspath(os.path.dirname(__file__))}/../../../instance" - terraform = shutil.which("terraform") - output = subprocess.run( - [terraform, f"-chdir={terraform_dir}", "output", "-raw", output_name], - cwd=terraform_dir, - capture_output=True, - env={"AWS_PROFILE": "apim-dev"}, - text=True, - ) - if output.returncode != 0: - raise RuntimeError(f"Error getting terraform output {output_name}: {output.stderr}") - return output.stdout.strip() - - -def get_s3_source_name(): - return _get_terraform_output("batch_source_bucket") - - -def get_s3_destination_name(): - return _get_terraform_output("batch_destination_bucket") - - -def get_cluster_name(): - return _get_terraform_output("batch_cluster_name") - - -def download_report_file(s3_client, bucket, key) -> str: - obj = s3_client.get_object(Bucket=bucket, Key=key) - data = obj["Body"].read() - return data.decode("utf-8") - - -@dataclass -class CtlData: - from_dts: str - to_dts: str - - def to_xml(self) -> str: - return f""" - - 1.0 - DTS - DATA - {self.from_dts} - {self.to_dts} - file_type=csv;sender_id=NBORetry - Tester01 - MPTREQ_20181114134908 - Y - N - VACCINATIONS_DAILY_COVID_4 - - - Y - Y - -""" - - -class CtlFile: - def __init__(self, ctl_data: CtlData): - self.stream = io.BytesIO() - self.ctl_data = ctl_data - self.stream.write(self.ctl_data.to_xml().encode()) - - # def upload_to_s3(self, s3_client, bucket, key): - # self.stream.seek(0) - # s3_client.upload_fileobj(self.stream, bucket, key) - # self.stream.close() - - def upload_to_s3(self, s3_client, bucket): - self.stream.seek(0) - s3_client.upload_fileobj(self.stream, bucket) - self.stream.close() - - -class BatchFile: - # each record holds a message and a record - records: List[Tuple[str, OrderedDict[str, str]]] = [] - headers: List[str] = [] - - def __init__(self, headers: List[str] = None): - if headers is None: - headers = base_headers - self.headers = headers - self.stream = io.BytesIO() - - _make_header(self.stream, self.headers) - - def add_record(self, record: OrderedDict[str, str], msg: str = ""): - if len(record.values()) != len(self.headers): - raise ValueError("record does not have the correct number of fields") - if self.stream.closed: - raise ValueError("batch file is closed") - self.records.append((msg, record)) - _make_row_entry(record, self.stream) - - # def upload_to_s3(self, s3_client, bucket, key): - # self.stream.seek(0) - # s3_client.upload_fileobj( - # self.stream, bucket, key, ExtraArgs={"ContentType": "text/plain"} - # ) - # self.stream.close() - - def upload_to_s3(self, s3_client, bucket): - self.stream.seek(0) - s3_client.upload_fileobj(self.stream, bucket, ExtraArgs={"ContentType": "text/plain"}) - self.stream.close() diff --git a/tests/e2e/utils/constants.py b/tests/e2e/utils/constants.py deleted file mode 100644 index 1527e7da06..0000000000 --- a/tests/e2e/utils/constants.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -valid_nhs_number1 = "9693632109" -valid_nhs_number2 = "9693633687" - -cis2_user = "656005750104" - -patient_identifier_system = "https://fhir.nhs.uk/Id/nhs-number" -valid_patient_identifier1 = f"{patient_identifier_system}|{valid_nhs_number1}" -valid_patient_identifier2 = f"{patient_identifier_system}|{valid_nhs_number2}" -identifier_system = "https://supplierABC/identifiers/vacc" -identifier_value = "f10b59b3-fc73-4616-99c9-9e882ab31184" - -env_internal_dev = os.environ.get("ENVIRONMENT", "") == "internal-dev" diff --git a/tests/e2e/utils/delete_sqs_messages.py b/tests/e2e/utils/delete_sqs_messages.py deleted file mode 100644 index d5ed82e2a1..0000000000 --- a/tests/e2e/utils/delete_sqs_messages.py +++ /dev/null @@ -1,32 +0,0 @@ -import boto3 -from botocore.exceptions import ClientError - - -def read_and_delete_messages(queue_url): - """ - Reads and deletes messages from an SQS queue. - Args: - queue_url: The URL of the SQS queue. - """ - sqs_client = boto3.client("sqs") - try: - # Receive messages with a maximum of 10 messages per request - response = sqs_client.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10) - - # Check if there are any messages - if "Messages" not in response: - # print("No messages found in the queue.") - return - # Process and delete each message - for message in response["Messages"]: - # Access message body - # message_body = message["Body"] - # Process the message (replace with your actual processing logic) - # print(f"Processing message: {message_body}") - # Get the receipt handle for deletion - receipt_handle = message["ReceiptHandle"] - # Delete the message from the queue - sqs_client.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) - # print(f"Deleted message: {message_body}") - except ClientError as e: - print(f"Error accessing SQS: {e}") diff --git a/tests/e2e/utils/factories.py b/tests/e2e/utils/factories.py deleted file mode 100644 index 7ae027300d..0000000000 --- a/tests/e2e/utils/factories.py +++ /dev/null @@ -1,177 +0,0 @@ -import os -import uuid -from typing import Set - -from lib.apigee import ApigeeApp, ApigeeConfig, ApigeeProduct, ApigeeService -from lib.authentication import ( - AppRestrictedAuthentication, - AppRestrictedCredentials, - AuthType, - UserRestrictedCredentials, -) -from lib.env import ( - get_apigee_access_token, - get_apigee_env, - get_apigee_username, - get_auth_url, - get_default_app_restricted_credentials, - get_proxy_name, -) -from lib.jwks import JwksData -from utils.authorization import ( - Permission, - app_full_access, - make_permissions_attribute, - make_vaxx_permissions_attribute, -) - -JWKS_PATH = f"{os.getcwd()}/.well-known" -PRIVATE_KEY_PATH = f"{os.getcwd()}/.keys" - - -def make_apigee_service(config: ApigeeConfig = None) -> ApigeeService: - config = ( - config - if config - else ApigeeConfig( - username=get_apigee_username(), - access_token=get_apigee_access_token(), - env=get_apigee_env(), - ) - ) - return ApigeeService(config) - - -def make_app_restricted_auth( - config: AppRestrictedCredentials = None, -) -> AppRestrictedAuthentication: - """If config is None, then we fall back to the default client configuration from env vars. Useful for Int env""" - config = config if config else get_default_app_restricted_credentials() - return AppRestrictedAuthentication(auth_url=get_auth_url(), config=config) - - -def make_apigee_product(apigee: ApigeeService = None, product: ApigeeProduct = None) -> ApigeeProduct: - if not apigee: - apigee = make_apigee_service() - if not product: - proxies = [ - f"identity-service-{get_apigee_env()}", - f"identity-service-mock-{get_apigee_env()}", - ] - product = ApigeeProduct( - name=str(uuid.uuid4()), - scopes=[ - f"urn:nhsd:apim:app:level3:{get_proxy_name()}", - f"urn:nhsd:apim:user-nhs-cis2:aal3:{get_proxy_name()}", - ], - proxies=proxies, - ) - - resp = apigee.create_product(product) - return ApigeeProduct.from_dict(resp) - - -def make_app_restricted_app( - apigee: ApigeeService = None, - app: ApigeeApp = None, - permissions: Set[Permission] = None, - vaxx_type_perms: Set = None, -) -> (ApigeeApp, AppRestrictedCredentials): - if not apigee: - apigee = make_apigee_service() - - use_default_app = app is None - if use_default_app: - cred = get_default_app_restricted_credentials() - stored_app = ApigeeApp(name="default-app") - return stored_app, cred - else: - # We use this prefix for file names. This way we don't create a separate file for each jwks - key_id_prefix = get_proxy_name() - # NOTE: adding uuid is important. identity-service caches the key_id so, - # this way we know it'll be invalidated each time we create a new jwks - key_id = f"{key_id_prefix}-{str(uuid.uuid4())}" - - jwks_data = JwksData(key_id) - jwks_url = jwks_data.get_jwks_url(base_url="https://api.service.nhs.uk/mock-jwks") - app.add_attribute("jwks-resource-url", jwks_url) - - if permissions := permissions or app_full_access(): - k, v = make_permissions_attribute(permissions) - app.add_attribute(k, v) - app.add_attribute("AuthenticationType", AuthType.APP_RESTRICTED.value) - app.add_attribute("SupplierSystem", "Test_App") - if vaxx_type_perms: - k, v = make_vaxx_permissions_attribute(vaxx_type_perms) - app.add_attribute(k, v) - else: - app.add_attribute( - "VaccineTypePermissions", - "flu:create,covid:create,mmr:create,hpv:create,covid:update,flu:read,covid:read,flu:delete," - "covid:delete,mmr:delete,flu:search,covid:search,mmr:search,rsv:create,rsv:search,rsv:update," - "rsv:read,rsv:delete", - ) - - app.add_product(f"identity-service-{get_apigee_env()}") - - resp = apigee.create_application(app) - stored_app = ApigeeApp.from_dict(resp) - - credentials = AppRestrictedCredentials( - client_id=stored_app.get_client_id(), - kid=key_id, - private_key_content=jwks_data.private_key, - ) - - return stored_app, credentials - - -def _make_user_restricted_app( - auth_type: AuthType, - apigee: ApigeeService = None, - app: ApigeeApp = None, - permissions: Set[Permission] = None, - vaxx_type_perms: Set = None, -) -> ApigeeApp: - if not apigee: - apigee = make_apigee_service() - - use_default_app = app is None - if use_default_app: - raise NotImplementedError("Default app for user-restricted is not implemented") - else: - if permissions := permissions or app_full_access(): - k, v = make_permissions_attribute(permissions) - app.add_attribute(k, v) - app.add_attribute("AuthenticationType", auth_type.value) - app.add_attribute("SupplierSystem", "Test_App") - if vaxx_type_perms: - k, v = make_vaxx_permissions_attribute(vaxx_type_perms) - app.add_attribute(k, v) - else: - app.add_attribute( - "VaccineTypePermissions", - "flu:create,covid:create,mmr:create,hpv:create,covid:update,flu:read,covid:read,flu:delete," - "covid:delete,mmr:delete,flu:search,covid:search,mmr:search,rsv:create,rsv:search,rsv:update," - "rsv:read,rsv:delete", - ) - app.add_product(f"identity-service-{get_apigee_env()}") - - resp = apigee.create_application(app) - return ApigeeApp.from_dict(resp) - - -def make_cis2_app( - apigee: ApigeeService = None, - app: ApigeeApp = None, - permissions: Set[Permission] = None, - vaxx_type_perms: Set = None, -) -> (ApigeeApp, UserRestrictedCredentials): - stored_app = _make_user_restricted_app(AuthType.CIS2, apigee, app, permissions, vaxx_type_perms) - credentials = UserRestrictedCredentials( - client_id=stored_app.get_client_id(), - client_secret=stored_app.get_client_secret(), - callback_url=stored_app.callbackUrl, - ) - - return stored_app, credentials diff --git a/tests/e2e/utils/get_sqs_url.py b/tests/e2e/utils/get_sqs_url.py deleted file mode 100644 index b818d5e4fe..0000000000 --- a/tests/e2e/utils/get_sqs_url.py +++ /dev/null @@ -1,22 +0,0 @@ -import boto3 -from botocore.exceptions import ClientError - - -def get_queue_url(queue_name): - """ - Retrieves the URL of an SQS queue by its name. - Args: - queue_name: The name of the SQS queue. - Returns: - The URL of the SQS queue, or None if not found. - """ - sqs_client = boto3.client("sqs") - try: - response = sqs_client.get_queue_url(QueueName=queue_name) - return response["QueueUrl"] - except ClientError as e: - if e.response["Error"]["Code"] == "QueueDoesNotExist": - print(f"Queue with name {queue_name} does not exist.") - else: - print(f"Error getting queue URL: {e}") - return None diff --git a/tests/e2e/utils/immunisation_api.py b/tests/e2e/utils/immunisation_api.py deleted file mode 100644 index ea27479070..0000000000 --- a/tests/e2e/utils/immunisation_api.py +++ /dev/null @@ -1,258 +0,0 @@ -import random -import re -import time -import uuid -from datetime import datetime -from typing import List, Literal, Optional - -import requests -from lib.authentication import BaseAuthentication -from utils.resource import delete_imms_records, generate_imms_resource - -from .constants import patient_identifier_system - - -def parse_location(location) -> Optional[str]: - """parse location header and return resource ID""" - pattern = r"https://.*\.api\.service\.nhs\.uk/immunisation-fhir-api.*/Immunization/(.+)" - if match := re.search(pattern, location): - return match.group(1) - else: - return None - - -class ImmunisationApi: - url: str - headers: dict - auth: BaseAuthentication - generated_test_records: List[str] - - def __init__(self, url, auth: BaseAuthentication): - self.url = url - - self.auth = auth - # NOTE: this class doesn't support refresh token or expiry check. - # This shouldn't be a problem in tests, just something to be aware of - token = self.auth.get_access_token() - self.headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/fhir+json", - "Accept": "application/fhir+json", - } - self.generated_test_records = [] - - def __str__(self): - return f"ImmunizationApi: AuthType: {self.auth}" - - # We implemented this function as a wrapper around the calls to APIGEE - # in order to prevent build pipelines from failing due to timeouts. - # The e2e tests put pressure on both test environments from APIGEE and PDS - # so the chances of having rate limiting errors are high especially during - # the busy times of the day. - @staticmethod - def make_request_with_backoff( - http_method: str, - url: str, - headers: dict = None, - expected_status_code: int = 200, - expected_connection_failure: bool = False, - max_retries: int = 5, - is_status_check: bool = False, - **kwargs, - ): - for attempt in range(max_retries): - try: - response = requests.request(method=http_method, url=url, headers=headers, **kwargs) - - # This property is false by default and only true during the mtls test to simulate a connection failure - if expected_connection_failure: - raise RuntimeError( - f"Expected the connection to fail, " - f"but it succeeded instead.\n" - f"Request method: {http_method}\n" - f"URL: {url}" - ) - - # Sometimes it can take time for the new endpoint to activate - if is_status_check: - body = response.json() - if body["status"].lower() != "pass": - raise RuntimeError( - f"Server status check at {url} returned status code {response.status_code}, " - f"but status is: {body['status']}" - ) - - # Check if the response matches the expected status code to identify potential issues - if response.status_code != expected_status_code: - if response.status_code >= 500: - raise RuntimeError(f"Server error: {response.status_code} during in {http_method} {url}") - else: - raise ValueError( - f"Expected {expected_status_code} but got {response.status_code} in {http_method} {url}" - ) - - return response - - except Exception as e: - if expected_connection_failure or attempt == max_retries - 1: - raise - - # This is will be used in the retry logic of the exponential backoff - delay = (3**attempt) + random.uniform(0, 0.5) - print( - f"[{datetime.now():%Y-%m-%d %H:%M:%S}] " - f"[Retry {attempt + 1}] {http_method.upper()} {url} — {e} — retrying in {delay:.2f}s" - ) - - time.sleep(delay) - - def create_immunization_resource(self, resource: dict = None) -> str: - """creates an Immunization resource and returns the resource id by parsing the resource url""" - imms = resource if resource else generate_imms_resource() - response = self.create_immunization(imms) - assert response.status_code == 201, (response.status_code, response.text) - return parse_location(response.headers["Location"]) - - def create_a_deleted_immunization_resource(self, resource: dict = None) -> dict: - """it creates a new Immunization and then delete it, it returns the created imms""" - imms = resource if resource else generate_imms_resource() - response = self.create_immunization(imms) - assert response.status_code == 201, response.text - imms_id = parse_location(response.headers["Location"]) - response = self.delete_immunization(imms_id) - assert response.status_code == 204, response.text - imms["id"] = str(uuid.uuid4()) - - return imms - - def get_immunization_by_id(self, event_id, expected_status_code: int = 200): - return self.make_request_with_backoff( - http_method="GET", - url=f"{self.url}/Immunization/{event_id}", - headers=self._update_headers(), - expected_status_code=expected_status_code, - ) - - # Create a new Immunization resource by sending a POST request to the API - # The function also validates the response and extracts the resource ID from the Location header - def create_immunization(self, imms, expected_status_code: int = 201): - response = self.make_request_with_backoff( - http_method="POST", - url=f"{self.url}/Immunization", - headers=self._update_headers(), - expected_status_code=expected_status_code, - json=imms, - ) - - if response.status_code == 201: - if "Location" not in response.headers: - raise ValueError("Missing 'Location' header in response") - - imms_id = response.headers["Location"].split("Immunization/")[-1] - if not self._is_valid_uuid4(imms_id): - raise ValueError(f"Invalid UUID4: {imms_id}") - - self.generated_test_records.append(imms_id) - - return response - - def update_immunization(self, imms_id, imms, expected_status_code: int = 200, headers=None): - return self.make_request_with_backoff( - http_method="PUT", - url=f"{self.url}/Immunization/{imms_id}", - headers=self._update_headers(headers), - expected_status_code=expected_status_code, - json=imms, - ) - - def delete_immunization(self, imms_id, expected_status_code: int = 204): - return self.make_request_with_backoff( - http_method="DELETE", - url=f"{self.url}/Immunization/{imms_id}", - headers=self._update_headers(), - expected_status_code=expected_status_code, - ) - - def search_immunizations( - self, - patient_identifier: str, - immunization_target: str, - expected_status_code: int = 200, - ): - return self.make_request_with_backoff( - http_method="GET", - url=f"{self.url}/Immunization?patient.identifier={patient_identifier_system}|{patient_identifier}" - f"&-immunization.target={immunization_target}", - headers=self._update_headers(), - expected_status_code=expected_status_code, - ) - - def search_immunization_by_identifier( - self, - identifier_system: str, - identifier_value: str, - expected_status_code: int = 200, - ): - return self.make_request_with_backoff( - http_method="GET", - url=f"{self.url}/Immunization?identifier={identifier_system}|{identifier_value}", - headers=self._update_headers(), - expected_status_code=expected_status_code, - ) - - def search_immunization_by_identifier_and_elements( - self, - identifier_system: str, - identifier_value: str, - expected_status_code: int = 200, - ): - return self.make_request_with_backoff( - http_method="GET", - url=f"{self.url}/Immunization?identifier={identifier_system}|{identifier_value}&_elements=id,meta", - headers=self._update_headers(), - expected_status_code=expected_status_code, - ) - - def search_immunizations_full( - self, - http_method: Literal["POST", "GET"], - query_string: Optional[str], - body: Optional[str], - expected_status_code: int = 200, - ): - if http_method == "POST": - url = f"{self.url}/Immunization/_search?{query_string}" - else: - url = f"{self.url}/Immunization?{query_string}" - - return self.make_request_with_backoff( - http_method=http_method, - url=url, - headers=self._update_headers({"Content-Type": "application/x-www-form-urlencoded"}), - expected_status_code=expected_status_code, - data=body, - ) - - def _update_headers(self, headers=None): - if headers is None: - headers = {} - updated = { - **self.headers, - **{ - "X-Correlation-ID": str(uuid.uuid4()), - "X-Request-ID": str(uuid.uuid4()), - "E-Tag": "1", - "Accept": "application/fhir+json", - }, - } - return {**updated, **headers} - - def _is_valid_uuid4(self, imms_id): - try: - val = uuid.UUID(imms_id, version=4) - return str(val) == imms_id - except ValueError: - return False - - def cleanup_test_records(self): - delete_imms_records(self.generated_test_records) diff --git a/tests/e2e/utils/mappings.py b/tests/e2e/utils/mappings.py deleted file mode 100644 index 29098ea98e..0000000000 --- a/tests/e2e/utils/mappings.py +++ /dev/null @@ -1,27 +0,0 @@ -class EndpointOperationNames: - """String enums for the name of each endpoint operation""" - - CREATE = "CREATE" - READ = "READ" - UPDATE = "UPDATE" - DELETE = "DELETE" - SEARCH = "SEARCH" - - -class VaccineTypes: - """Vaccine types""" - - covid: str = "COVID" - flu: str = "FLU" - hpv: str = "HPV" - mmr: str = "MMR" - rsv: str = "RSV" - - -vaccine_type_mappings = [ - (["840539006"], VaccineTypes.covid), - (["6142004"], VaccineTypes.flu), - (["240532009"], VaccineTypes.hpv), - (["14189004", "36653000", "36989005"], VaccineTypes.mmr), - (["55735004"], VaccineTypes.rsv), -] diff --git a/tests/e2e/utils/resource.py b/tests/e2e/utils/resource.py deleted file mode 100644 index 7bc7b3b987..0000000000 --- a/tests/e2e/utils/resource.py +++ /dev/null @@ -1,172 +0,0 @@ -import json -import os -import uuid -from copy import deepcopy -from decimal import Decimal -from typing import Literal, Union - -import boto3 -from botocore.config import Config -from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table - -from .constants import valid_nhs_number1 -from .mappings import VaccineTypes, vaccine_type_mappings - -current_directory = os.path.dirname(os.path.realpath(__file__)) - - -def load_example(path: str) -> dict: - with open(f"{current_directory}/../../../specification/components/examples/{path}") as f: - return json.load(f, parse_float=Decimal) - - -def generate_imms_resource( - nhs_number=valid_nhs_number1, - vaccine_type=VaccineTypes.covid, - imms_identifier_value: str = None, - occurrence_date_time: str = None, - sample_data_file_name: str = "completed_[vaccine_type]_immunization_event", -) -> dict: - """ - Creates a FHIR Immunization Resource dictionary, which includes an id, using the sample data for the given - vaccine type as a base, and updates the id, nhs_number and occurrence_date_time as required. - The unique_identifier is also updated to ensure uniqueness... - """ - # Load the data - sample_data_file_name = sample_data_file_name.replace("[vaccine_type]", vaccine_type.lower()) - imms = deepcopy(load_example(f"Immunization/{sample_data_file_name}.json")) - - # Apply identifier directly - imms["identifier"][0]["value"] = imms_identifier_value or str(uuid.uuid4()) - - if nhs_number is not None: - imms["contained"][1]["identifier"][0]["value"] = nhs_number - - if occurrence_date_time is not None: - imms["occurrenceDateTime"] = occurrence_date_time - - return imms - - -def generate_filtered_imms_resource( - crud_operation_to_filter_for: Literal["READ", "SEARCH", ""] = "", - nhs_number=valid_nhs_number1, - imms_identifier_value: str = None, - vaccine_type=VaccineTypes.covid, - occurrence_date_time: str = None, -) -> dict: - """ - Creates a filtered FHIR Immunization Resource dictionary, which includes an id, using the sample filtered data for - the given vaccine type, crud operation (if specified) as a base, and updates the id, - nhs_number and occurrence_date_time as required. - - NOTE: The filtered sample data files use the corresponding unfiltered sample data files as a base, and this - function can therefore be used in combination with the create_an_imms_obj function for testing filtering. - NOTE: New sample data files can be added by copying the sample data file for the releavant vaccine type and - removing, obfuscating or amending the relevant fields as required. - The new file name must be consistent with the existing sample data file names. - """ - # Load the data - file_name = ( - f"Immunization/completed_{vaccine_type.lower()}_immunization_event" - + f"_filtered_for_{crud_operation_to_filter_for.lower()}" - ) - imms = deepcopy(load_example(f"{file_name}.json")) - # Apply identifier directly - imms["identifier"][0]["value"] = imms_identifier_value or str(uuid.uuid4()) - - # Note that NHS number is found in a different place on a search return - if crud_operation_to_filter_for == "SEARCH": - imms["patient"]["identifier"]["value"] = nhs_number - else: - imms["contained"][1]["identifier"][0]["value"] = nhs_number - - if occurrence_date_time is not None: - imms["occurrenceDateTime"] = occurrence_date_time - - return imms - - -def get_patient_id(imms: dict) -> str: - patients = [resource for resource in imms["contained"] if resource["resourceType"] == "Patient"] - return patients[0]["identifier"][0]["value"] - - -def disease_codes_to_vaccine_type(disease_codes_input: list) -> Union[str, None]: - """ - Takes a list of disease codes and returns the corresponding vaccine type if found, - otherwise raises a value error - """ - try: - return next( - vaccine_type - for disease_codes, vaccine_type in vaccine_type_mappings - if sorted(disease_codes_input) == disease_codes - ) - except Exception as e: - raise ValueError( - f"protocolApplied[0].targetDisease[*].coding[?(@.system=='http://snomed.info/sct')].code - " - f"{disease_codes_input} is not a valid combination of disease codes for this service" - ) from e - - -def get_vaccine_type(immunization: dict): - """ - Take a FHIR immunization resource and returns the vaccine type based on the combination of target diseases. - If combination of disease types does not map to a valid vaccine type, a value error is raised - """ - try: - target_diseases = [] - target_disease_list = immunization["protocolApplied"][0]["targetDisease"] - for element in target_disease_list: - code = [x.get("code") for x in element["coding"] if x.get("system") == "http://snomed.info/sct"][0] - target_diseases.append(code) - except (KeyError, IndexError, AttributeError) as error: - raise ValueError("No target disease codes found") from error - return disease_codes_to_vaccine_type(target_diseases) - - -def get_patient_postal_code(imms: dict): - patients = [record for record in imms.get("contained", []) if record.get("resourceType") == "Patient"] - if patients: - return patients[0]["address"][0]["postalCode"] - return "" - - -def get_full_row_from_identifier(identifier: str) -> dict: - table = get_dynamodb_table(os.getenv("DYNAMODB_TABLE_NAME")) - return table.get_item(Key={"PK": f"Immunization#{identifier}"}).get("Item") - - -def get_dynamodb_table(table_name: str) -> Table: - config = Config(connect_timeout=60, read_timeout=60, retries={"max_attempts": 3}) - db: DynamoDBServiceResource = boto3.resource("dynamodb", region_name="eu-west-2", config=config) - return db.Table(table_name) - - -def delete_imms_records(identifiers: list[str]) -> None: - """Batch delete immunization records from the DynamoDB table. - Handles logging internally and does not return anything. - """ - table = get_dynamodb_table(os.getenv("DYNAMODB_TABLE_NAME")) - success_count = 0 - failure_count = 0 - total = len(identifiers) - - if total > 0: - try: - with table.batch_writer(overwrite_by_pkeys=["PK"]) as batch: - for identifier in identifiers: - key = {"PK": f"Immunization#{identifier}"} - try: - batch.delete_item(Key=key) - success_count += 1 - except Exception as e: - print(f"Failed to delete record with key {key}: {e}") - failure_count += 1 - except Exception as e: - print(f"[teardown error] Batch writer failed: {e}") - failure_count = total # Assume all failed if batch writer fails - - if failure_count > 0: - print(f"[teardown warning] Deleted {success_count} records out of {total}, failed to delete {failure_count}") diff --git a/tests/e2e_automation/.env.example.dynamic b/tests/e2e_automation/.env.example.dynamic new file mode 100644 index 0000000000..fad5d06f0b --- /dev/null +++ b/tests/e2e_automation/.env.example.dynamic @@ -0,0 +1,24 @@ +# Do not change these 3 values. We always use internal-dev for dynamic mock auth in all APIM Apigee non-prod environments +auth_url=https://internal-dev.api.service.nhs.uk/oauth2-mock/authorize +token_url=https://internal-dev.api.service.nhs.uk/oauth2-mock/token +callback_url=https://oauth.pstmn.io/v1/callback + +# Obtain value from dev/testing team +STATUS_API_KEY= +username= +scope= + +# Set all the below values based on the environment you are testing e.g. pr-xxx, internal-dev, internal-qa +baseUrl=https://internal-dev.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4-pr-123 +aws_token_refresh=False +aws_profile_name={your-aws-profile} + +S3_env=pr-123 +LOCAL_RUN_FILE_NAME=HPV_Vaccinations_v5_V0V8L_20251111T16304982.csv +AWS_DOMAIN_NAME=pr-123.imms.dev.vds.platform.nhs.uk + +PROXY_NAME=immunisation-fhir-api-pr-123 +# See README for details on how to obtain this +APIGEE_ACCESS_TOKEN={use-the-apigee-get-token-utility} +APIGEE_USERNAME={your-apigee-developer-email} +APIGEE_ENVIRONMENT={the-relevant-apigee-env}# E.g. internal-dev, internal-qa diff --git a/tests/e2e_automation/.env.example.static b/tests/e2e_automation/.env.example.static new file mode 100644 index 0000000000..592b67d394 --- /dev/null +++ b/tests/e2e_automation/.env.example.static @@ -0,0 +1,58 @@ +aws_token_refresh=True +USE_STATIC_APPS=True + +########## INT env variables +# baseUrl=https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4 +# auth_url=https://int.api.service.nhs.uk/oauth2-mock/authorize +# token_url=https://int.api.service.nhs.uk/oauth2-mock/token +# callback_url=https://oauth.pstmn.io/v1/callback +# S3_env=int +# aws_profile_name={your-aws-profile} +# PROXY_NAME=immunisation-fhir-api-int +## Id/Secret values - please contact Dev or Test team to get these values +# username= +# scope= +# STATUS_API_KEY= +# AWS_DOMAIN_NAME= +# Postman_Auth_client_Id= +# Postman_Auth_client_Secret= +# RAVS_client_Id= +# RAVS_client_Secret= +# MAVIS_client_Id= +# MAVIS_client_Secret= +# EMIS_client_Id= +# EMIS_client_Secret= +# SONAR_client_Id= +# SONAR_client_Secret= +# TPP_client_Id= +# TPP_client_Secret= +# MEDICUS_client_Id= +# MEDICUS_client_Secret= + +########## Internal-QA env variables +baseUrl=https://internal-qa.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4 +auth_url=https://internal-qa.api.service.nhs.uk/oauth2-mock/authorize +token_url=https://internal-qa.api.service.nhs.uk/oauth2-mock/token +callback_url=https://oauth.pstmn.io/v1/callback +S3_env=internal-qa +aws_profile_name={your-aws-profile} +PROXY_NAME=immunisation-fhir-api-internal-qa +## Id/Secret values - please contact Dev or Test team to get these values +username= +scope= +STATUS_API_KEY= +AWS_DOMAIN_NAME= +Postman_Auth_client_Id= +Postman_Auth_client_Secret= +RAVS_client_Id= +RAVS_client_Secret= +MAVIS_client_Id= +MAVIS_client_Secret= +EMIS_client_Id= +EMIS_client_Secret= +SONAR_client_Id= +SONAR_client_Secret= +TPP_client_Id= +TPP_client_Secret= +MEDICUS_client_Id= +MEDICUS_client_Secret= diff --git a/tests/e2e_automation/Makefile b/tests/e2e_automation/Makefile new file mode 100644 index 0000000000..3f8bedca9b --- /dev/null +++ b/tests/e2e_automation/Makefile @@ -0,0 +1,24 @@ +-include .env + +cmd = poetry run pytest + +test: + $(cmd) + +test-api-smoke: + $(cmd) features/APITests -m smoke + +test-api-full: + $(cmd) features/APITests -m functional + +test-batch-smoke: + $(cmd) features/batchTests -m smoke + +test-batch-full: + $(cmd) features/batchTests -m functional + +test-sandbox: + $(cmd) features -m sandbox + +collect-only: + $(cmd) --collect-only diff --git a/tests/e2e_automation/README.md b/tests/e2e_automation/README.md new file mode 100644 index 0000000000..f448328948 --- /dev/null +++ b/tests/e2e_automation/README.md @@ -0,0 +1,61 @@ +# End-to-end Automation Tests + +This directory contains End-to-end Automation Tests for the Immunisation FHIR API. + +## Setting up e2e_automation tests to run locally + +1. Follow the instructions in the root level README.md to setup the [dependencies](../../README.md#environment-setup) and create a [virtual environment](../../README.md#setting-up-a-virtual-environment-with-poetry) for this folder (`e2e_automation`). +2. Run `poetry install --no-root` to install dependencies. +3. Ensure you are authenticated with AWS in your terminal, using your preferred method. +4. Choose which approach you wish to use: static apps or temporary apps. Temporary apps are the recommended way to go as + they are spun up and torn down on each test run, therefore requiring fewer env vars and less operational overhead. Skip + to the relevant section below depending on which approach you are going to use. + +## Setup when using on temporary apps + +**NOTE:** this approach cannot be used in INT. Both INT and PROD (we do not test here) belong to the APIM Apigee prod +organisation so there is no support for creating apps on the fly. + +**Background:** this approach uses the Apigee API to create and teardown applications during a test run. This is the +approach used in the pipeline for all APIM non-prod environments: internal-dev, internal-qa, pr and so forth. + +1. Configure your .env file with the required values. + + The [.env.example.dynamic](./.env.example.dynamic) template defines all the values you will need and explains how + to obtain them. Most will be simple enough, based on the environment you wish to test. However, there is the + `STATUS_API_KEY` which you will need to ask the testers or tech lead for, and `APIGEE_ACCESS_TOKEN` which is outlined + below. + +2. [Install](https://docs.apigee.com/api-platform/system-administration/auth-tools#install) and run the Apigee [get_token](https://docs.apigee.com/api-platform/system-administration/using-gettoken) tool to obtain an access token. +3. Set this value against the `APIGEE_ACCESS_TOKEN` variable in your .env file. +4. Finally, use the Makefile to run your desired suite of tests. + +Note: the `get_token` tool is only supported in Linux environments, so if you are using a Windows environment, you will +at least need to run the operation in WSL to obtain the access token. + +## Setup when using static apps + +**NOTE:** you must use this approach in INT. + +1. Configure your .env file with the required values. + + The [.env.example.static](./.env.example.static) template defines all the values you will need and explains how + to obtain them. A lot more configuration is required as you will need to obtain all the `{Supplier}_client_Id` and + `{Supplier}_client_Secret` values for the static apps in your target environment. + +2. Finally, use the Makefile to run your desired suite of tests. + +## Test commands + +The `Makefile` in this directory provides the following commands: + +- `make test` - run all tests (may take some time) +- `make test-api-full` - run API tests +- `make test-api-smoke` - run API smoke tests only (quicker) +- `make test-batch-full` - run Batch tests +- `make test-batch-smoke` - run Batch smoke tests only (quicker) +- `make test-sandbox` - run lightweight tests for the sandbox i.e. just checks /\_ping and /\_status +- `make collect-only` - check that all tests are discovered + +If you want even more granular control, you can run `poetry run pytest features -m` followed by the given suite you +want to run e.g. `Delete_Feature`, `Create_Batch_Feature` and so on. diff --git a/tests/e2e/__init__.py b/tests/e2e_automation/features/APITests/__init__.py similarity index 100% rename from tests/e2e/__init__.py rename to tests/e2e_automation/features/APITests/__init__.py diff --git a/tests/e2e_automation/features/APITests/create.feature b/tests/e2e_automation/features/APITests/create.feature new file mode 100644 index 0000000000..4c96444f0f --- /dev/null +++ b/tests/e2e_automation/features/APITests/create.feature @@ -0,0 +1,232 @@ +@Create_Feature @functional +Feature: Create the immunization event for a patient + +@Delete_cleanUp @smoke +Scenario Outline: Verify that the POST Create API for different vaccine types + Given Valid token is generated for the '' + And Valid json payload is created with Patient '' and vaccine_type '' + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The imms event table will be populated with the correct data for 'created' event + And The delta table will be populated with the correct data for created event + + Examples: + | Patient | vaccine_type| Supplier | + |Random | COVID | Postman_Auth | + |Random | RSV | RAVS | + |Random | FLU | MAVIS | + |Random | MMR | Postman_Auth | + |Random | MENACWY | TPP | + |Random | 3IN1 | TPP | + |Random | MMRV | EMIS | + |Random | PERTUSSIS | EMIS | + |Random | SHINGLES | EMIS | + |Random | PNEUMOCOCCAL| EMIS | + |Random | 4IN1 | TPP | + |Random | 6IN1 | EMIS | + |Random | HIB | TPP | + |Random | MENB | TPP | + |Random | ROTAVIRUS | MEDICUS | + |Random | HEPB | EMIS | + |Random | BCG | MEDICUS | + +@Delete_cleanUp @vaccine_type_6IN1 @patient_id_Random @supplier_name_EMIS +Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to respective text fields in imms delta table + Given Valid json payload is created where vaccination terms has text field populated + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + And The terms are mapped to the respective text fields in imms delta table + +@Delete_cleanUp @vaccine_type_BCG @patient_id_Random @supplier_name_EMIS +Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM fields are mapped to first instance of coding.display fields in imms delta table + Given Valid json payload is created where vaccination terms has multiple instances of coding + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + And The terms are mapped to first instance of coding.display fields in imms delta table + +@Delete_cleanUp @vaccine_type_HEPB @patient_id_Random @supplier_name_MEDICUS +Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to correct instance of coding.display fields in imms delta table + Given Valid json payload is created where vaccination terms has multiple instance of coding with different coding system + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + And The terms are mapped to correct instance of coding.display fields in imms delta table + +@Delete_cleanUp @vaccine_type_PERTUSSIS @patient_id_Random @supplier_name_EMIS +Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to coding.display in imms delta table in case of only one instance of coding + Given Valid json payload is created where vaccination terms has one instance of coding with no text or value string field + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + And The terms are mapped to correct coding.display fields in imms delta table + +@Delete_cleanUp @vaccine_type_HIB @patient_id_Random @supplier_name_TPP +Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are blank in imms delta table if no text or value string or display field is present + Given Valid json payload is created where vaccination terms has no text or value string or display field + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + And The terms are blank in imms delta table + +Scenario Outline: Verify that the POST Create API for different supplier fails on access denied + Given Valid token is generated for the '' + And Valid json payload is created with Patient '' and vaccine_type '' + When Trigger the post create request + Then The request will be unsuccessful with the status code '403' + And The Response JSONs should contain correct error message for 'unauthorized_access' access + Examples: + | Patient | vaccine_type| Supplier | + |Random | COVID | MAVIS | + |Random | RSV | MAVIS | + |Random | RSV | SONAR | + +@Delete_cleanUp @supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Mod11_NHS +Scenario: Verify that the POST Create API for invalid but Mod11 compliant NHS Number + Given Valid json payload is created + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The imms event table will be populated with the correct data for 'created' event + And The delta table will be populated with the correct data for created event + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if doseNumberPositiveInt is not valid + Given Valid json payload is created where doseNumberPositiveInt is '' + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | doseNumberPositiveInt | error_type | + | -1 | doseNumberPositiveInt_PositiveInteger | + | 0 | doseNumberPositiveInt_PositiveInteger | + | 10 | doseNumberPositiveInt_ValidRange | + + +@Delete_cleanUp @supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario: Verify that the POST Create API will be successful if all date field has valid past date + Given Valid json payload is created where date fields has past date + When Trigger the post create request + Then The request will be successful with the status code '201' + And The location key and Etag in header will contain the Immunization Id and version + + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if occurrenceDateTime has future or invalid formatted date + Given Valid json payload is created where occurrenceDateTime has invalid '' date + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | Date | error_type | + | future_occurrence | invalid_OccurrenceDateTime | + | invalid_format | invalid_OccurrenceDateTime | + | nonexistent | invalid_OccurrenceDateTime | + | empty | invalid_OccurrenceDateTime | + | none | empty_OccurrenceDateTime | + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if recorded has future or invalid formatted date + Given Valid json payload is created where recorded has invalid '' date + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | Date | error_type | + | future_date | invalid_recorded | + | invalid_format | invalid_recorded | + | nonexistent | invalid_recorded | + | empty | invalid_recorded | + | none | empty_recorded | + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if patient's data of birth has future or invalid formatted date + Given Valid json payload is created where date of birth has invalid '' date + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | Date | error_type | + | future_date | future_DateOfBirth | + | invalid_format | invalid_DateOfBirth | + | nonexistent | invalid_DateOfBirth | + | empty | invalid_DateOfBirth | + | none | missing_DateOfBirth | + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if expiration date has invalid formatted date + Given Valid json payload is created where expiration date has invalid '' date + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for 'invalid_expirationDate' + Examples: + | Date | + | invalid_format | + | nonexistent | + | empty | + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if nhs number is invalid + Given Valid json payload is created where Nhs number is invalid '' + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + |invalid_NhsNumber |error_type | + |1234567890 |invalid_mod11_nhsnumber | + |12345678 |invalid_nhsnumber_length | + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if patient forename is invalid + Given Valid json payload is created where patient forename is '' + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | forename | error_type | + | empty | empty_forename | + | missing | no_forename | + | white_space_array | empty_forename | + | single_value_max_len | max_len_forename | + | max_len_array | max_item_forename | + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if patient surname is invalid + Given Valid json payload is created where patient surname is '' + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | surname | error_type | + | empty | empty_surname | + | missing | no_surname | + | white_space | empty_surname | + | name_length_36 | max_len_surname | + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario: Verify that the POST Create API will fail if patient name is empty + Given Valid json payload is created where patient name is empty + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for 'empty_forename_surname' + +@supplier_name_Postman_Auth @vaccine_type_RSV @patient_id_Random +Scenario Outline: Verify that the POST Create API will fail if patient gender is invalid + Given Valid json payload is created where patient gender is '' + When Trigger the post create request + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | gender | error_type | + | random_text | invalid_gender | + | empty | empty_gender | + | number | should_be_string | + | gender_code | invalid_gender | + | missing | missing_gender | + + + diff --git a/tests/e2e_automation/features/APITests/delete.feature b/tests/e2e_automation/features/APITests/delete.feature new file mode 100644 index 0000000000..7779b0511c --- /dev/null +++ b/tests/e2e_automation/features/APITests/delete.feature @@ -0,0 +1,38 @@ +@Delete_Feature @functional +Feature: Delete an immunization of a patient + +@vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the Delete API will be successful with all the valid parameters + Given I have created a valid vaccination record + When Send a delete for Immunization event created + Then The request will be successful with the status code '204' + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The imms event table will be populated with the correct data for 'deleted' event + And The delta table will be populated with the correct data for deleted event + +@vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the Delete event is not coming in Get Search API response + Given I have created a valid vaccination record + When Send a delete for Immunization event created + Then The request will be successful with the status code '204' + And Deleted Immunization event will not be present in Get method Search API response + +@vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the Delete event is not coming in Post Search API response + Given I have created a valid vaccination record + When Send a delete for Immunization event created + Then The request will be successful with the status code '204' + And Deleted Immunization event will not be present in Post method Search API response + +@vaccine_type_RSV @patient_id_Random +Scenario: Verify that the Delete event request will fail with unauthorized access for MAVIS supplier + Given valid vaccination record is created by 'RAVS' supplier + When Send a delete for Immunization event created for the above created event is send by 'MAVIS' + Then The request will be successful with the status code '403' + And The Response JSONs should contain correct error message for 'unauthorized_access' access + +@vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the Delete event request will fail with Not found error + When Send a delete for Immunization event created with invalid Imms Id + Then The request will be successful with the status code '404' + And The Response JSONs should contain correct error message for Imms_id 'not_found' \ No newline at end of file diff --git a/tests/e2e_automation/features/APITests/read.feature b/tests/e2e_automation/features/APITests/read.feature new file mode 100644 index 0000000000..08766d0bcb --- /dev/null +++ b/tests/e2e_automation/features/APITests/read.feature @@ -0,0 +1,44 @@ +@Read_Feature @functional +Feature: Read the immunization of a patient + +@Delete_cleanUp @supplier_name_MEDICUS +Scenario Outline: Verify that the Read method of API will be successful and fetch valid imms event detail + Given Valid vaccination record is created with Patient '' and vaccine_type '' + When Send a read request for Immunization event created + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The Read Response JSONs field values should match with the input JSONs field values + + Examples: + |Patient | Vaccine_type| + |Random | 4IN1 | + |Random | FLU | + |Random | COVID | + + +@Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Flu event is created and updated twice and read request fetch the latest version and Etag + Given I have created a valid vaccination record + And created event is being updated twice + When Send a read request for Immunization event created + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The Read Response JSONs field values should match with the input JSONs field values + +@vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Deleted event returns 404 on read request + Given I have created a valid vaccination record + And created event is being deleted + When Send a read request for Immunization event created + Then The request will be unsuccessful with the status code '404' + And The Response JSONs should contain correct error message for 'not_found' + +@vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the Read event request will fail with Not found error with invalid Imms Id + When Send a read request for Immunization event created with invalid Imms Id + Then The request will be unsuccessful with the status code '404' + And The Response JSONs should contain correct error message for Imms_id 'not_found' + + diff --git a/tests/e2e_automation/features/APITests/search.feature b/tests/e2e_automation/features/APITests/search.feature new file mode 100644 index 0000000000..1a32709515 --- /dev/null +++ b/tests/e2e_automation/features/APITests/search.feature @@ -0,0 +1,234 @@ +@Search_Feature @functional +Feature: Search the immunization of a patient + +@smoke +@Delete_cleanUp @supplier_name_TPP +Scenario Outline: Verify that the GET method of Search API will be successful with all the valid parameters + Given Valid vaccination record is created with Patient '' and vaccine_type '' + When Send a search request with GET method for Immunization event created + Then The request will be successful with the status code '200' + And The Search Response JSONs should contain the detail of the immunization events created above + And The Search Response JSONs field values should match with the input JSONs field values for resourceType Immunization + And The Search Response JSONs field values should match with the input JSONs field values for resourceType Patient + Examples: + |Patient | Vaccine_type | + |Random | MMRV | + |SFlag | RSV | + |SupersedeNhsNo| RSV | + |Random | FLU | + |SFlag | FLU | + |SupersedeNhsNo| FLU | + |Random | COVID | + |SFlag | PERTUSSIS | + |SupersedeNhsNo| COVID | + |Mod11_NHS | RSV | + |Random | SHINGLES | + |Random | PNEUMOCOCCAL | + +@smoke +@Delete_cleanUp @supplier_name_EMIS +Scenario Outline: Verify that the POST method of Search API will be successful with all the valid parameters + Given Valid vaccination record is created with Patient '' and vaccine_type '' + When Send a search request with POST method for Immunization event created + Then The request will be successful with the status code '200' + And The Search Response JSONs should contain the detail of the immunization events created above + And The Search Response JSONs field values should match with the input JSONs field values for resourceType Immunization + And The Search Response JSONs field values should match with the input JSONs field values for resourceType Patient + Examples: + |Patient | Vaccine_type| + |Random | RSV | + |SFlag | SHINGLES | + |SupersedeNhsNo| PERTUSSIS | + |Random | FLU | + |SFlag | 3IN1 | + |SupersedeNhsNo| 4IN1 | + |Random | COVID | + |SFlag | BCG | + |SupersedeNhsNo| HEPB | + +@supplier_name_Postman_Auth +Scenario Outline: Verify that the immunisation events retrieved in the response of Search API should be within Date From and Date To range + When Send a search request with GET method with valid NHS Number '' and Disease Type '' and Date From '' and Date To '' + Then The request will be successful with the status code '200' + And The occurrenceDateTime of the immunization events should be within the Date From and Date To range + When Send a search request with POST method with valid NHS Number '' and Disease Type '' and Date From '' and Date To '' + Then The request will be successful with the status code '200' + And The occurrenceDateTime of the immunization events should be within the Date From and Date To range + Examples: + |NHSNumber | vaccine_type | DateFrom | DateTo | + |9728403348 | FLU | 2025-01-01 | 2025-06-04 | + +# Negative Scenarios +@supplier_name_Postman_Auth +Scenario Outline: Verify that Search API will throw error if NHS Number is invalid + When Send a search request with GET method with invalid NHS Number '' and valid Disease Type '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid NHS Number + When Send a search request with POST method with invalid NHS Number '' and valid Disease Type '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid NHS Number + Examples: + | NHSNumber | DiseaseType | + | "" | COVID | + | 1234567890 | RSV | + | 1 | COVID | + | 10000000000 00001 | COVID | + + +@supplier_name_Postman_Auth +Scenario Outline: Verify that Search API will throw error if include is invalid + When Send a search request with GET method with valid NHS Number '' and valid Disease Type '' and invalid include '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid include + When Send a search request with POST method with valid NHS Number '' and valid Disease Type '' and invalid include '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid include + Examples: + |NHSNumber | vaccine_type | include | + |9728403348 | COVID | abc | + + +@supplier_name_Postman_Auth +Scenario Outline: Verify that Search API will throw error if both different combination of dates and include is invalid + When Send a search request with GET method with valid NHS Number '' and valid Disease Type '' and Date From '' and Date To '' and include '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date From, Date To and include + When Send a search request with POST method with valid NHS Number '' and valid Disease Type '' and Date From '' and Date To '' and include '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date From, Date To and include + Examples: + |NHSNumber | vaccine_type | DateFrom | DateTo | include | + |9728403348 | COVID | 999-06-01 | 999-06-01 | abc | + |9728403348 | COVID | 2025-13-01 | 2025-12-01 | abc | + |9728403348 | COVID | 2025-05-12 | 2025-05-12 | abc | + |9728403348 | COVID | 999-06-01 | 999-06-01 | Immunization:patient | + +@supplier_name_Postman_Auth +Scenario Outline: Verify that Search API will throw error if Disease Type is invalid + When Send a search request with GET method with valid NHS Number '' and invalid Disease Type '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Disease Type + When Send a search request with POST method with valid NHS Number '' and invalid Disease Type '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Disease Type + Examples: + | NHSNumber | DiseaseType | + | 9449304424 | "" | + | 9449304424 | FLu | + | 9449304424 | ABC | + +@supplier_name_Postman_Auth +Scenario Outline: Verify that Search API will throw error if both NHS Number and Disease Type are invalid + When Send a search request with GET method with invalid NHS Number '' and invalid Disease Type '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid NHS Number as higher priority + When Send a search request with POST method with invalid NHS Number '' and invalid Disease Type '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid NHS Number as higher priority + Examples: + | NHSNumber | DiseaseType | + | 1234567890 | ABC | + | "" | "" | + +@supplier_name_MAVIS @vaccine_type_RSV +Scenario Outline: Verify that Search API will throw error if date from is invalid + When Send a search request with GET method with invalid Date From '' and valid Date To '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date From + When Send a search request with POST method with invalid Date From '' and valid Date To '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date From + Examples: + | DateFrom | DateTo | + | 999-06-01 | 2025-06-01 | + | 2025-13-01 | 2025-06-01 | + | 2025-05-32 | 2025-06-01 | + +@supplier_name_RAVS @vaccine_type_RSV +Scenario Outline: Verify that Search API will throw error if date to is invalid + When Send a search request with GET method with valid Date From '' and invalid Date To '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date To + When Send a search request with POST method with valid Date From '' and invalid Date To '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date To + Examples: + | DateFrom | DateTo | + | 2025-05-01 | 999-06-01 | + | 2025-05-01 | 2025-13-01 | + | 2025-05-01 | 2025-05-32 | + +@supplier_name_MAVIS @vaccine_type_RSV +Scenario Outline: Verify that Search API will throw error if both date from and date to are invalid + When Send a search request with GET method with invalid Date From '' and invalid Date To '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date From + When Send a search request with POST method with invalid Date From '' and invalid Date To '' + Then The request will be unsuccessful with the status code '400' + And The Search Response JSONs should contain correct error message for invalid Date From + Examples: + | DateFrom | DateTo | + | 999-06-01 | 999-06-01 | + | 2025-13-01 | 2025-13-01 | + | 2025-05-32 | 2025-05-32 | + + +@supplier_name_SONAR +Scenario Outline: Verify that Search API will throw error supplier is not authorized to make Search + When Send a search request with GET method with invalid NHS Number '' and valid Disease Type '' + Then The request will be unsuccessful with the status code '403' + And The Response JSONs should contain correct error message for 'unauthorized_access' access + When Send a search request with POST method with invalid NHS Number '' and valid Disease Type '' + Then The request will be unsuccessful with the status code '403' + And The Response JSONs should contain correct error message for 'unauthorized_access' access + Examples: + | NHSNumber | DiseaseType | + | 9449304424 | COVID | + + +@Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Flu event is created and updated twice and search request fetch the latest meta version and Etag + Given I have created a valid vaccination record + And created event is being updated twice + When Send a search request with GET method for Immunization event created + Then The request will be successful with the status code '200' + And The Search Response JSONs should contain the detail of the immunization events created above + And The Search Response JSONs field values should match with the input JSONs field values for resourceType Immunization + And The Search Response JSONs field values should match with the input JSONs field values for resourceType Patient + +@Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Flu event is created and search post request fetch the only one record matched with identifier + Given I have created a valid vaccination record + When Send a search request with Post method using identifier header for Immunization event created + Then The request will be successful with the status code '200' + And correct immunization event is returned in the response + +@Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Flu event is created and search post request fetch the only one record matched with identifier and _elements + Given I have created a valid vaccination record + When Send a search request with Post method using identifier and _elements header for Immunization event created + Then The request will be successful with the status code '200' + And correct immunization event is returned in the response with only specified elements + +@Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Flu event is created and search post request fetch the only one record matched with identifier with correct version id + Given I have created a valid vaccination record + And created event is being updated twice + When Send a search request with Post method using identifier header for Immunization event created + Then The request will be successful with the status code '200' + And correct immunization event is returned in the response + +@Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Flu event is created and search post request fetch the only one record matched with identifier and _elements with correct version id + Given I have created a valid vaccination record + And created event is being updated twice + When Send a search request with Post method using identifier and _elements header for Immunization event created + Then The request will be successful with the status code '200' + And correct immunization event is returned in the response with only specified elements + +@vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Empty search response will be received when no record is found for the given identifier in search request + When Send a search request with post method using invalid identifier header for Immunization event created + Then The request will be successful with the status code '200' + And Empty immunization event is returned in the response + \ No newline at end of file diff --git a/tests/e2e_automation/features/APITests/status.feature b/tests/e2e_automation/features/APITests/status.feature new file mode 100644 index 0000000000..4eb8d5d72c --- /dev/null +++ b/tests/e2e_automation/features/APITests/status.feature @@ -0,0 +1,23 @@ +@Status_feature @functional +Feature: Get the API status + +@smoke @sandbox +Scenario: Verify that the /ping endpoint works + When I send a request to the ping endpoint + Then The request will be successful with the status code '200' + +@smoke @sandbox +Scenario: Verify that the /status endpoint works + When I send a request to the status endpoint + Then The request will be successful with the status code '200' + And The status response will contain a passing healthcheck + +@smoke +Scenario: Verify that clients cannot make a direct connection without mTLS to AWS backend + When I send a direct request to the AWS backend + Then The request is rejected + +@smoke +Scenario: Verify that unauthenticated clients cannot get a successful response from the API + When I send an unauthenticated request to the API + Then The request will be unsuccessful with the status code '401' diff --git a/tests/e2e/lib/__init__.py b/tests/e2e_automation/features/APITests/steps/__init__.py similarity index 100% rename from tests/e2e/lib/__init__.py rename to tests/e2e_automation/features/APITests/steps/__init__.py diff --git a/tests/e2e_automation/features/APITests/steps/common_steps.py b/tests/e2e_automation/features/APITests/steps/common_steps.py new file mode 100644 index 0000000000..c64efe57ba --- /dev/null +++ b/tests/e2e_automation/features/APITests/steps/common_steps.py @@ -0,0 +1,288 @@ +import json +import random +from urllib.parse import parse_qs +from venv import logger + +import pytest_check as check +from pytest_bdd import given, parsers, then, when +from src.dynamoDB.dynamo_db_helper import fetch_immunization_events_detail, parse_imms_int_imms_event_response +from src.objectModels.api_immunization_builder import ( + build_site_route, + build_vaccine_procedure_extension, + convert_to_update, + create_immunization_object, + get_vaccine_details, +) +from src.objectModels.patient_loader import load_patient_by_id +from utilities.api_fhir_immunization_helper import ( + is_valid_disease_type, + is_valid_nhs_number, + parse_error_response, + validate_error_response, + validate_to_compare_request_and_response, +) +from utilities.api_gen_token import get_tokens +from utilities.api_get_header import get_create_post_url_header, get_delete_url_header, get_update_url_header +from utilities.date_helper import is_valid_date +from utilities.enums import Operation +from utilities.http_requests_session import http_requests_session +from utilities.vaccination_constants import ROUTE_MAP, SITE_MAP + + +@given(parsers.parse("Valid token is generated for the '{Supplier}'")) +def valid_token_is_generated(context, Supplier): + context.supplier_name = Supplier + get_tokens(context, Supplier) + + +@given("Valid json payload is created") +def valid_json_payload_is_created(context): + context.patient = load_patient_by_id(context.patient_id) + context.immunization_object = create_immunization_object(context.patient, context.vaccine_type) + + +@given(parsers.parse("Valid json payload is created with Patient '{Patient}' and vaccine_type '{vaccine_type}'")) +def The_Immunization_object_is_created_with_patient_for_vaccine_type(context, Patient, vaccine_type): + context.vaccine_type = vaccine_type + context.patient_id = Patient + context.patient = load_patient_by_id(context.patient_id) + context.immunization_object = create_immunization_object(context.patient, context.vaccine_type) + + +@given(parsers.parse("Valid vaccination record is created with Patient '{Patient}' and vaccine_type '{vaccine_type}'")) +def validVaccinationRecordIsCreatedWithPatient(context, Patient, vaccine_type): + The_Immunization_object_is_created_with_patient_for_vaccine_type(context, Patient, vaccine_type) + Trigger_the_post_create_request(context) + The_request_will_have_status_code(context, 201) + validateCreateLocation(context) + + +@given("I have created a valid vaccination record") +def validVaccinationRecordIsCreated(context): + valid_json_payload_is_created(context) + Trigger_the_post_create_request(context) + The_request_will_have_status_code(context, 201) + validateCreateLocation(context) + + +@given(parsers.parse("valid vaccination record is created by '{Supplier}' supplier")) +def valid_vaccination_record_is_created_by_supplier(context, Supplier): + valid_token_is_generated(context, Supplier) + validVaccinationRecordIsCreated(context) + + +@when("Trigger the post create request") +def Trigger_the_post_create_request(context): + get_create_post_url_header(context) + context.create_object = context.immunization_object + context.request = context.create_object.dict(exclude_none=True, exclude_unset=True) + context.response = http_requests_session.post(context.url, json=context.request, headers=context.headers) + print(f"Create Request is {json.dumps(context.request)}") + + +@then(parsers.parse("The request will be unsuccessful with the status code '{statusCode}'")) +@then(parsers.parse("The request will be successful with the status code '{statusCode}'")) +def The_request_will_have_status_code(context, statusCode): + print(context.response.status_code) + print(int(statusCode)) + assert context.response.status_code == int(statusCode), ( + f"\n Expected status code: {statusCode}, but got: {context.response.status_code}. Response: {context.response.json()} \n" + ) + + +@then("The location key and Etag in header will contain the Immunization Id and version") +def validateCreateLocation(context): + location = context.response.headers["location"] + eTag = context.response.headers["E-Tag"] + assert "location" in context.response.headers, ( + f"Location header is missing in the response with Status code: {context.response.statusCode}. Response: {context.response.json()}" + ) + assert "E-Tag" in context.response.headers, ( + f"E-Tag header is missing in the response with Status code: {context.response.statusCode}. Response: {context.response.json()}" + ) + context.ImmsID = location.split("/")[-1] + context.eTag = eTag.strip('"') + print(f"\n Immunization ID is {context.ImmsID} and Etag is {context.eTag} \n") + check.is_true( + context.ImmsID is not None, + f"Expected IdentifierPK: {context.patient.identifier[0].value}, Found: {context.ImmsID}", + ) + + +@then("The Search Response JSONs should contain correct error message for invalid NHS Number") +@then("The Search Response JSONs should contain correct error message for invalid Disease Type") +@then("The Search Response JSONs should contain correct error message for invalid Date From") +@then("The Search Response JSONs should contain correct error message for invalid Date To") +@then("The Search Response JSONs should contain correct error message for invalid NHS Number as higher priority") +@then("The Search Response JSONs should contain correct error message for invalid include") +@then("The Search Response JSONs should contain correct error message for invalid Date From and Date To") +@then("The Search Response JSONs should contain correct error message for invalid Date From, Date To and include") +def operationOutcomeInvalidParams(context): + error_response = parse_error_response(context.response.json()) + params = getattr(context, "params", getattr(context, "request", {})) + + if isinstance(params, str): + parsed = parse_qs(params) + params = {k: v[0] for k, v in parsed.items()} if parsed else {} + + date_from_value = params.get("-date.from") + date_to_value = params.get("-date.to") + include_value = params.get("_include") + nhs_number = params.get("patient.identifier").replace("https://fhir.nhs.uk/Id/nhs-number|", "") + disease_type = params.get("-immunization.target") + + # Validation flags + nhs_invalid = not is_valid_nhs_number(nhs_number) + disease_invalid = not is_valid_disease_type(disease_type) + date_from_invalid = date_from_value and not is_valid_date(date_from_value) + date_to_invalid = date_to_value and not is_valid_date(date_to_value) + include_invalid = include_value != "Immunization:patient" + + match (nhs_invalid, disease_invalid, date_from_invalid, date_to_invalid, include_invalid): + case (True, _, _, _, _): + expected_error = "invalid_NHSNumber" + case (False, True, _, _, _): + expected_error = "invalid_DiseaseType" + case (False, False, True, True, False): + expected_error = "invalid_DateFrom_To" + case (False, False, True, True, True): + expected_error = "invalid_DateFrom_DateTo_Include" + case (False, False, True, _, True): + expected_error = "invalid_DateFrom_Include" + case (False, False, True, _, _): + expected_error = "invalid_DateFrom" + case (False, False, _, True, _): + expected_error = "invalid_DateTo" + case (False, False, _, _, True): + expected_error = "invalid_include" + case _: + raise ValueError("All parameters are valid, no error expected.") + + validate_error_response(error_response, expected_error) + print(f"\n Error Response - \n {error_response}") + + +@then("The X-Request-ID and X-Correlation-ID keys in header will populate correctly") +def validateCreateHeader(context): + assert "X-Request-ID" in context.response.request.headers, "X-Request-ID missing in headers" + assert "X-Correlation-ID" in context.response.request.headers, "X-Correlation-ID missing in headers" + assert context.response.request.headers["X-Request-ID"] == context.reqID, "X-Request-ID incorrect" + assert context.response.request.headers["X-Correlation-ID"] == context.corrID, "X-Correlation-ID incorrect" + + +@then(parsers.parse("The imms event table will be populated with the correct data for '{operation}' event")) +def validate_imms_event_table_by_operation(context, operation: Operation): + create_obj = context.create_object + table_query_response = fetch_immunization_events_detail(context.aws_profile_name, context.ImmsID, context.S3_env) + assert "Item" in table_query_response, f"Item not found in response for ImmsID: {context.ImmsID}" + item = table_query_response["Item"] + + resource_json_str = item.get("Resource") + assert resource_json_str, "Resource field missing in item." + + try: + resource = json.loads(resource_json_str) + except (TypeError, json.JSONDecodeError) as e: + logger.error(f"Failed to parse Resource from item: {e}") + raise AssertionError("Failed to parse Resource from response item.") + + assert resource is not None, "Resource is None in the response" + created_event = parse_imms_int_imms_event_response(resource) + + assert int(context.expected_version) == int(context.eTag), ( + f"Expected Version: {context.expected_version}, Found: {context.eTag}" + ) + + fields_to_compare = [ + ("Operation", Operation[operation].value, item.get("Operation")), + ("SupplierSystem", context.supplier_name.upper(), item.get("SupplierSystem").upper()), + ("PatientPK", f"Patient#{context.patient.identifier[0].value}", item.get("PatientPK")), + ("PatientSK", f"{context.vaccine_type.upper()}#{context.ImmsID}", item.get("PatientSK")), + ("Version", int(context.expected_version), int(item.get("Version"))), + ] + + for name, expected, actual in fields_to_compare: + check.is_true(expected == actual, f"Expected {name}: {expected}, Actual {actual}") + + validate_to_compare_request_and_response(context, create_obj, created_event, True) + + +@then(parsers.parse("The Response JSONs should contain correct error message for '{errorName}'")) +@then(parsers.parse("The Response JSONs should contain correct error message for '{errorName}' access")) +@then(parsers.parse("The Response JSONs should contain correct error message for Imms_id '{errorName}'")) +def validateForbiddenAccess(context, errorName): + error_response = parse_error_response(context.response.json()) + validate_error_response(error_response, errorName, imms_id=context.ImmsID) + print(f"\n Error Response - \n {error_response}") + + +@then("The Etag in header will containing the latest event version") +def validate_etag_in_header(context): + etag = context.response.headers["E-Tag"] + assert etag, "Etag header is missing in the response" + context.eTag = etag.strip('"') + assert context.eTag == str(context.expected_version), ( + f"Etag version mismatch: expected {context.expected_version}, got {context.eTag}" + ) + + +@when("Send a update for Immunization event created with vaccination detail being updated") +def send_update_for_vaccination_detail(context): + get_update_url_header(context, str(context.expected_version)) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + context.expected_version = int(context.expected_version) + 1 + context.update_object.extension = [build_vaccine_procedure_extension(context.vaccine_type.upper())] + vaccine_details = get_vaccine_details(context.vaccine_type.upper()) + context.update_object.vaccineCode = vaccine_details["vaccine_code"] + context.update_object.site = build_site_route(random.choice(SITE_MAP)) + context.update_object.route = build_site_route(random.choice(ROUTE_MAP)) + context.create_object = context.update_object + context.request = context.update_object.dict(exclude_none=True, exclude_unset=True) + context.response = http_requests_session.put( + context.url + "/" + context.ImmsID, json=context.request, headers=context.headers + ) + print(f"Update Request is {json.dumps(context.request)}") + + +@when("Send a update for Immunization event created with patient address being updated") +def send_update_for_immunization_event(context): + get_update_url_header(context, str(context.expected_version)) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + context.update_object.contained[1].address[0].city = "Updated City" + context.update_object.contained[1].address[0].state = "Updated State" + trigger_the_updated_request(context) + + +@given("created event is being updated twice") +def created_event_is_being_updated_twice(context): + send_update_for_immunization_event(context) + The_request_will_have_status_code(context, 200) + send_update_for_vaccination_detail(context) + The_request_will_have_status_code(context, 200) + + +@given("created event is being deleted") +def created_event_is_being_deleted(context): + send_delete_for_immunization_event_created(context) + The_request_will_have_status_code(context, 204) + + +@when("Send a delete for Immunization event created") +def send_delete_for_immunization_event_created(context): + get_delete_url_header(context) + print(f"\n Delete Request is {context.url}/{context.ImmsID}") + context.response = http_requests_session.delete(f"{context.url}/{context.ImmsID}", headers=context.headers) + + +def trigger_the_updated_request(context): + context.expected_version = int(context.expected_version) + 1 + context.create_object = context.update_object + context.request = context.update_object.dict(exclude_none=True, exclude_unset=True) + context.response = http_requests_session.put( + context.url + "/" + context.ImmsID, json=context.request, headers=context.headers + ) + print(f"Update Request is {json.dumps(context.request)}") + + +def normalize_param(value: str) -> str: + return "" if value.lower() in {"none", "null", ""} else value diff --git a/tests/e2e_automation/features/APITests/steps/test_create_steps.py b/tests/e2e_automation/features/APITests/steps/test_create_steps.py new file mode 100644 index 0000000000..a27d2ee50d --- /dev/null +++ b/tests/e2e_automation/features/APITests/steps/test_create_steps.py @@ -0,0 +1,317 @@ +import json +import random +from datetime import datetime, timedelta, timezone +from venv import logger + +import pytest_check as check +from pytest_bdd import given, parsers, scenarios, then +from src.dynamoDB.dynamo_db_helper import ( + fetch_immunization_events_detail, + fetch_immunization_int_delta_detail_by_immsID, + get_all_term_text, + get_all_the_vaccination_codes, + parse_imms_int_imms_event_response, + validate_imms_delta_record_with_created_event, +) +from src.objectModels.api_immunization_builder import ( + build_site_route, + build_vaccine_procedure_code, + build_vaccine_procedure_extension, + get_vaccine_details, +) +from utilities.api_fhir_immunization_helper import validate_to_compare_request_and_response +from utilities.date_helper import generate_date +from utilities.enums import ActionFlag, Operation +from utilities.text_helper import get_text +from utilities.vaccination_constants import ROUTE_MAP, SITE_MAP, VACCINATION_PROCEDURE_MAP, VACCINE_CODE_MAP + +from .common_steps import valid_json_payload_is_created + +scenarios("APITests/create.feature") + + +@given(parsers.parse("Valid json payload is created where doseNumberPositiveInt is '{doseNumberPositiveInt}'")) +def createValidJsonPayloadWithDoseNumberPositiveInt(context, doseNumberPositiveInt): + valid_json_payload_is_created(context) + context.immunization_object.protocolApplied[0].doseNumberPositiveInt = int(doseNumberPositiveInt) + + +@given("Valid json payload is created where date fields has past date") +def create_valid_json_payload_with_past_dates(context): + valid_json_payload_is_created(context) + today = datetime.now(timezone.utc) + context.immunization_object.contained[1].birthDate = str((today - timedelta(days=150)).date()) + context.immunization_object.occurrenceDateTime = str((today - timedelta(days=15)).isoformat(timespec="milliseconds")) + context.immunization_object.recorded = str((today - timedelta(days=20)).date()) + context.immunization_object.expirationDate = str((today + timedelta(days=5)).date()) + + +@given(parsers.parse("Valid json payload is created where occurrenceDateTime has invalid '{DateText}' date")) +def createValidJsonPayloadWithInvalidOccurrenceDateTime(context, DateText): + valid_json_payload_is_created(context) + context.immunization_object.occurrenceDateTime = generate_date(DateText) + + +@given(parsers.parse("Valid json payload is created where recorded has invalid '{DateText}' date")) +def createValidJsonPayloadWithInvalidRecorded(context, DateText): + valid_json_payload_is_created(context) + context.immunization_object.recorded = generate_date(DateText) + + +@given(parsers.parse("Valid json payload is created where expiration date has invalid '{DateText}' date")) +def createValidJsonPayloadWithInvalidExpiration(context, DateText): + valid_json_payload_is_created(context) + context.immunization_object.expirationDate = generate_date(DateText) + + +@given(parsers.parse("Valid json payload is created where date of birth has invalid '{DateText}' date")) +def createValidJsonPayloadWithInvalidDOB(context, DateText): + valid_json_payload_is_created(context) + context.immunization_object.contained[1].birthDate = generate_date(DateText) + + +@given("Valid json payload is created where vaccination terms has text field populated") +def createValidJsonPayloadWithProcedureText(context): + valid_json_payload_is_created(context) + context.immunization_object.extension = [ + build_vaccine_procedure_extension(context.vaccine_type.upper(), "testing procedure term text") + ] + vaccine_details = get_vaccine_details(context.vaccine_type.upper(), "testing product term text") + context.immunization_object.vaccineCode = vaccine_details["vaccine_code"] + context.immunization_object.site = build_site_route(random.choice(SITE_MAP), "testing site text") + context.immunization_object.route = build_site_route(random.choice(ROUTE_MAP), "testing route text") + + +@given("Valid json payload is created where vaccination terms has multiple instances of coding") +def createValidJsonPayloadWithProcedureMultipleCodings(context): + valid_json_payload_is_created(context) + procedures_list = get_all_the_vaccination_codes(VACCINATION_PROCEDURE_MAP[context.vaccine_type.upper()]) + product_list = get_all_the_vaccination_codes(VACCINE_CODE_MAP[context.vaccine_type.upper()]) + + context.immunization_object.extension[0].valueCodeableConcept.coding = procedures_list + context.immunization_object.vaccineCode.coding = product_list + + +@given( + "Valid json payload is created where vaccination terms has multiple instance of coding with different coding system" +) +def createValidJsonPayloadWithProcedureMultipleCodingsDifferentSystem(context): + createValidJsonPayloadWithProcedureMultipleCodings(context) + site_list = get_all_the_vaccination_codes(SITE_MAP) + route_list = get_all_the_vaccination_codes(ROUTE_MAP) + context.immunization_object.site.coding = site_list + context.immunization_object.route.coding = route_list + + context.immunization_object.extension[0].valueCodeableConcept.coding[ + 0 + ].system = "http://example.com/different-system" + context.immunization_object.vaccineCode.coding[0].system = "http://example.com/different-system" + context.immunization_object.site.coding[0].system = "http://example.com/different-system" + context.immunization_object.route.coding[0].system = "http://example.com/different-system" + + +@given( + "Valid json payload is created where vaccination terms has one instance of coding with no text or value string field" +) +def createValidJsonPayloadWithProcedureNoTextValue(context): + valid_json_payload_is_created(context) + context.immunization_object.extension[0].valueCodeableConcept = build_vaccine_procedure_code( + context.vaccine_type.upper(), add_extensions=False + ) + vaccine_details = get_vaccine_details(context.vaccine_type.upper(), add_extensions=False) + context.immunization_object.vaccineCode = vaccine_details["vaccine_code"] + + context.immunization_object.site = build_site_route(random.choice(SITE_MAP), add_extensions=False) + context.immunization_object.route = build_site_route(random.choice(ROUTE_MAP), add_extensions=False) + + +@given("Valid json payload is created where vaccination terms has no text or value string or display field") +def createValidJsonPayloadWithProcedureNoTextValueDisplay(context): + valid_json_payload_is_created(context) + context.immunization_object.extension[0].valueCodeableConcept.coding[0].extension = None + context.immunization_object.extension[0].valueCodeableConcept.coding[0].display = None + context.immunization_object.extension[0].valueCodeableConcept.text = None + context.immunization_object.vaccineCode.coding[0].extension = None + context.immunization_object.vaccineCode.coding[0].display = None + context.immunization_object.vaccineCode.text = None + context.immunization_object.site.coding[0].extension = None + context.immunization_object.site.coding[0].display = None + context.immunization_object.site.text = None + context.immunization_object.route.coding[0].extension = None + context.immunization_object.route.coding[0].display = None + context.immunization_object.route.text = None + + +@then("The imms event table will be populated with the correct data for created event") +def validate_imms_event_table(context): + create_obj = context.create_object + table_query_response = fetch_immunization_events_detail(context.aws_profile_name, context.ImmsID, context.S3_env) + assert "Item" in table_query_response, f"Item not found in response for ImmsID: {context.ImmsID}" + item = table_query_response["Item"] + + resource_json_str = item.get("Resource") + assert resource_json_str, "Resource field missing in item." + + try: + resource = json.loads(resource_json_str) + except (TypeError, json.JSONDecodeError) as e: + logger.error(f"Failed to parse Resource from item: {e}") + raise AssertionError("Failed to parse Resource from response item.") + + assert resource is not None, "Resource is None in the response" + created_event = parse_imms_int_imms_event_response(resource) + + fields_to_compare = [ + ( + "IdentifierPK", + f"{create_obj.identifier[0].system}#{create_obj.identifier[0].value}", + item.get("IdentifierPK"), + ), + ("Operation", Operation.created.value, item.get("Operation")), + ("PatientPK", f"Patient#{context.patient.identifier[0].value}", item.get("PatientPK")), + ("PatientSK", f"{context.vaccine_type.upper()}#{context.ImmsID}", item.get("PatientSK")), + ("SupplierSystem", context.supplier_name.lower(), item.get("SupplierSystem").lower()), + ("Version", 1, item.get("Version")), + ] + + for name, expected, actual in fields_to_compare: + check.is_true(expected == actual, f"Expected {name}: {expected}, Actual {actual}") + + validate_to_compare_request_and_response(context, create_obj, created_event, True) + + +@then("The delta table will be populated with the correct data for created event") +def validate_imms_delta_table_by_ImmsID(context): + create_obj = context.create_object + item = fetch_immunization_int_delta_detail_by_immsID( + context.aws_profile_name, context.ImmsID, context.S3_env, context.expected_version + ) + assert item, f"Item not found in response for ImmsID: {context.ImmsID}" + + validate_imms_delta_record_with_created_event( + context, create_obj, item, Operation.created.value, ActionFlag.created.value + ) + + +@then("The terms are mapped to the respective text fields in imms delta table") +def validate_procedure_term_text_in_delta_table(context): + actual_terms = get_all_term_text(context) + assert actual_terms["procedure_term"] == context.create_object.extension[0].valueCodeableConcept.text, ( + f"Expected procedure term '{context.create_object.extension[0].valueCodeableConcept.text}', but got '{actual_terms['procedure_term']}'" + ) + assert actual_terms["product_term"] == context.create_object.vaccineCode.text, ( + f"Expected product term '{context.create_object.vaccineCode.text}', but got '{actual_terms['product_term']}'" + ) + assert actual_terms["site_term"] == context.create_object.site.text, ( + f"Expected site of vaccination term '{context.create_object.site.text}', but got '{actual_terms['site_term']}'" + ) + assert actual_terms["route_term"] == context.create_object.route.text, ( + f"Expected route of vaccination term '{context.create_object.route.text}', but got '{actual_terms['route_term']}'" + ) + print( + "\n The delta table fields covered are VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM\n" + ) + + +@then("The terms are mapped to first instance of coding.display fields in imms delta table") +def validate_procedure_term_first_display_in_delta_table(context): + actual_terms = get_all_term_text(context) + assert actual_terms["procedure_term"] == context.create_object.extension[0].valueCodeableConcept.coding[0].display, ( + f"Expected procedure term '{context.create_object.extension[0].valueCodeableConcept.coding[0].display}', but got '{actual_terms['procedure_term']}'" + ) + assert actual_terms["product_term"] == context.create_object.vaccineCode.coding[0].display, ( + f"Expected product term '{context.create_object.vaccineCode.coding[0].display}', but got '{actual_terms['product_term']}'" + ) + + +@then("The terms are mapped to correct instance of coding.display fields in imms delta table") +def validate_procedure_term_correct_coding_in_delta_table(context): + actual_terms = get_all_term_text(context) + assert actual_terms["procedure_term"] == context.create_object.extension[0].valueCodeableConcept.coding[1].display, ( + f"Expected procedure term text '{context.create_object.extension[0].valueCodeableConcept.coding[1].display}', but got '{actual_terms['procedure_term']}'" + ) + assert actual_terms["product_term"] == context.create_object.vaccineCode.coding[1].display, ( + f"Expected product term '{context.create_object.vaccineCode.coding[1].display}', but got '{actual_terms['product_term']}'" + ) + assert actual_terms["site_term"] == context.create_object.site.coding[1].display, ( + f"Expected site of vaccination term '{context.create_object.site.coding[1].display}', but got '{actual_terms['site_term']}'" + ) + assert actual_terms["route_term"] == context.create_object.route.coding[1].display, ( + f"Expected route of vaccination term '{context.create_object.route.coding[1].display}', but got '{actual_terms['route_term']}'" + ) + + +@then("The terms are mapped to correct coding.display fields in imms delta table") +def validate_procedure_term_second_display_in_delta_table(context): + actual_terms = get_all_term_text(context) + assert actual_terms["procedure_term"] == context.create_object.extension[0].valueCodeableConcept.coding[0].display, ( + f"Expected procedure term text '{context.create_object.extension[0].valueCodeableConcept.coding[0].display}', but got '{actual_terms['procedure_term']}'" + ) + assert actual_terms["product_term"] == context.create_object.vaccineCode.coding[0].display, ( + f"Expected product term text '{context.create_object.vaccineCode.coding[0].display}', but got '{actual_terms['product_term']}'" + ) + assert actual_terms["site_term"] == context.create_object.site.coding[0].display, ( + f"Expected site of vaccination term text '{context.create_object.site.coding[0].display}', but got '{actual_terms['site_term']}'" + ) + assert actual_terms["route_term"] == context.create_object.route.coding[0].display, ( + f"Expected route of vaccination term text '{context.create_object.route.coding[0].display}', but got '{actual_terms['route_term']}'" + ) + + +@then("The terms are blank in imms delta table") +def validate_procedure_term_blank_in_delta_table(context): + actual_terms = get_all_term_text(context) + assert actual_terms["procedure_term"] == "", ( + f"Expected procedure term text to be blank, but got '{actual_terms['procedure_term']}'" + ) + assert actual_terms["product_term"] == "", ( + f"Expected product term text to be blank, but got '{actual_terms['product_term']}'" + ) + assert actual_terms["site_term"] == "", ( + f"Expected site of vaccination term text to be blank, but got '{actual_terms['site_term']}'" + ) + assert actual_terms["route_term"] == "", ( + f"Expected route of vaccination term text to be blank, but got '{actual_terms['route_term']}'" + ) + + +@given(parsers.parse("Valid json payload is created where Nhs number is invalid '{invalid_NhsNumber}'")) +def create_request_with_invalid_Nhsnumber(context, invalid_NhsNumber): + valid_json_payload_is_created(context) + context.immunization_object.contained[1].identifier[0].value = invalid_NhsNumber + + +@given(parsers.parse("Valid json payload is created where patient forename is '{forename}'")) +def create_request_with_invalid_forename(context, forename): + valid_json_payload_is_created(context) + if forename == "single_value_max_len": + context.immunization_object.contained[1].name[0].given = [get_text("name_length_181")] + elif forename == "max_len_array": + context.immunization_object.contained[1].name[0].given = [ + get_text("name_length_15"), + get_text("name_length_5"), + get_text("name_length_5"), + get_text("name_length_10"), + get_text("name_length_10"), + get_text("name_length_10"), + ] + else: + context.immunization_object.contained[1].name[0].given = get_text(forename) + + +@given(parsers.parse("Valid json payload is created where patient surname is '{surname}'")) +def create_request_with_invalid_surname(context, surname): + valid_json_payload_is_created(context) + context.immunization_object.contained[1].name[0].family = get_text(surname) + + +@given(parsers.parse("Valid json payload is created where patient gender is '{gender}'")) +def create_request_with_invalid_gender(context, gender): + valid_json_payload_is_created(context) + context.immunization_object.contained[1].gender = get_text(gender) + + +@given("Valid json payload is created where patient name is empty") +def create_request_with_empty_nam(context): + valid_json_payload_is_created(context) + context.immunization_object.contained[1].name = None diff --git a/tests/e2e_automation/features/APITests/steps/test_delete_steps.py b/tests/e2e_automation/features/APITests/steps/test_delete_steps.py new file mode 100644 index 0000000000..28a348bac2 --- /dev/null +++ b/tests/e2e_automation/features/APITests/steps/test_delete_steps.py @@ -0,0 +1,87 @@ +import logging +import uuid + +from pytest_bdd import parsers, scenarios, then, when +from src.dynamoDB.dynamo_db_helper import ( + fetch_immunization_int_delta_detail_by_immsID, + validate_imms_delta_record_with_created_event, +) +from utilities.api_fhir_immunization_helper import find_entry_by_Imms_id, parse_FHIR_immunization_response +from utilities.api_get_header import get_delete_url_header +from utilities.enums import ActionFlag, Operation +from utilities.http_requests_session import http_requests_session + +from .common_steps import ( + The_request_will_have_status_code, + send_delete_for_immunization_event_created, + valid_token_is_generated, +) +from .test_search_steps import TriggerSearchGetRequest, TriggerSearchPostRequest + +logging.basicConfig(filename="debugLog.log", level=logging.INFO) +logger = logging.getLogger(__name__) + + +scenarios("APITests/delete.feature") + + +@when("Send a delete for Immunization event created with invalid Imms Id") +def send_delete_for_immunization_event_created_invalid(context): + get_delete_url_header(context) + context.ImmsID = str(uuid.uuid4()) + print(f"\n Delete Request is {context.url}/{context.ImmsID}") + context.response = http_requests_session.delete(f"{context.url}/{context.ImmsID}", headers=context.headers) + + +@when(parsers.parse("Send a delete for Immunization event created for the above created event is send by '{Supplier}'")) +def send_delete_for_immunization_event_by_supplier(context, Supplier): + valid_token_is_generated(context, Supplier) + send_delete_for_immunization_event_created(context) + + +@then("The delta table will be populated with the correct data for deleted event") +def validate_imms_delta_table_by_deleted_ImmsID(context): + create_obj = context.create_object + items = fetch_immunization_int_delta_detail_by_immsID(context.aws_profile_name, context.ImmsID, context.S3_env, 2) + assert items, f"Items not found in response for ImmsID: {context.ImmsID}" + + # Find the latest item where operation is DELETE + deleted_items = [i for i in items if i.get("Operation") == Operation.deleted.value] + assert deleted_items, f"No deleted item found for ImmsID: {context.ImmsID}" + + # Assuming each item has a 'timestamp' field to determine the latest + item = [max(deleted_items, key=lambda x: x.get("timestamp", 0))] + + validate_imms_delta_record_with_created_event( + context, create_obj, item, Operation.deleted.value, ActionFlag.deleted.value + ) + + +@then("Deleted Immunization event will not be present in Get method Search API response") +def validate_deleted_immunization_event_not_present(context): + TriggerSearchGetRequest(context) + The_request_will_have_status_code(context, "200") + + data = context.response.json() + context.parsed_search_object = parse_FHIR_immunization_response(data) + + context.created_event = find_entry_by_Imms_id(context.parsed_search_object, context.ImmsID) + + assert context.created_event is None, ( + f"Immunization event with ID {context.ImmsID} should not be present in the search response after deletion." + ) + + +@then("Deleted Immunization event will not be present in Post method Search API response") +def validate_deleted_immunization_event_not_present_using_post(context): + TriggerSearchPostRequest(context) + The_request_will_have_status_code(context, "200") + + data = context.response.json() + context.parsed_search_object = parse_FHIR_immunization_response(data) + + context.created_event = find_entry_by_Imms_id(context.parsed_search_object, context.ImmsID) + + assert context.created_event is None, ( + f"Immunization event with ID {context.ImmsID} should not be present in the search response after deletion." + ) diff --git a/tests/e2e_automation/features/APITests/steps/test_read_steps.py b/tests/e2e_automation/features/APITests/steps/test_read_steps.py new file mode 100644 index 0000000000..cb87108192 --- /dev/null +++ b/tests/e2e_automation/features/APITests/steps/test_read_steps.py @@ -0,0 +1,35 @@ +import logging +import uuid + +from pytest_bdd import scenarios, then, when +from utilities.api_fhir_immunization_helper import parse_read_response, validate_to_compare_request_and_response +from utilities.api_get_header import get_read_url_header +from utilities.http_requests_session import http_requests_session + +logging.basicConfig(filename="debugLog.log", level=logging.INFO) +logger = logging.getLogger(__name__) + +scenarios("APITests/read.feature") + + +@when("Send a read request for Immunization event created") +def send_read_for_immunization_event_created(context): + get_read_url_header(context) + print(f"\n Read Request is {context.url}") + context.response = http_requests_session.get(f"{context.url}", headers=context.headers) + + +@then("The Read Response JSONs field values should match with the input JSONs field values") +def the_read_response_jsons_field_values_should_match_with_the_input_jsons_field_values(context): + create_obj = context.create_object + data = context.response.json() + context.created_event = parse_read_response(data) + validate_to_compare_request_and_response(context, create_obj, context.created_event, True) + + +@when("Send a read request for Immunization event created with invalid Imms Id") +def send_read_for_immunization_event_created_with_invalid_imms_id(context): + context.ImmsID = str(uuid.uuid4()) + get_read_url_header(context) + print(f"\n Read Request is {context.url}") + context.response = http_requests_session.get(f"{context.url}", headers=context.headers) diff --git a/tests/e2e_automation/features/APITests/steps/test_search_steps.py b/tests/e2e_automation/features/APITests/steps/test_search_steps.py new file mode 100644 index 0000000000..6dd56e140d --- /dev/null +++ b/tests/e2e_automation/features/APITests/steps/test_search_steps.py @@ -0,0 +1,431 @@ +import uuid +from datetime import datetime +from urllib.parse import parse_qs + +import pytest_check as check +from pytest_bdd import parsers, scenarios, then, when +from src.objectModels.api_search_object import convert_to_form_data, set_request_data +from utilities.api_fhir_immunization_helper import ( + find_entry_by_Imms_id, + find_patient_by_fullurl, + parse_FHIR_immunization_response, + validate_to_compare_request_and_response, +) +from utilities.api_get_header import get_search_get_url_header, get_search_post_url_header +from utilities.date_helper import iso_to_compact +from utilities.http_requests_session import http_requests_session + +from .common_steps import normalize_param + +scenarios("APITests/search.feature") + + +@when("Send a search request with Post method using identifier header for Immunization event created") +def send_search_post_request_with_identifier_header(context): + get_search_post_url_header(context) + context.request = { + "identifier": f"{context.create_object.identifier[0].system}|{context.create_object.identifier[0].value}" + } + print(f"\n Search Post Request - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when("Send a search request with Post method using identifier and _elements header for Immunization event created") +def send_search_post_request_with_identifier_and_elements_header(context): + get_search_post_url_header(context) + context.request = { + "identifier": f"{context.create_object.identifier[0].system}|{context.create_object.identifier[0].value}", + "_elements": "meta,id", + } + print(f"\n Search Post Request - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when("Send a search request with post method using invalid identifier header for Immunization event created") +def send_search_post_request_with_invalid_identifier_header(context): + get_search_post_url_header(context) + context.request = {"identifier": f"https://www.ieds.england.nhs.uk/|{str(uuid.uuid4())}", "_elements": "meta,id"} + print(f"\n Search Post Request - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when("Send a search request with GET method for Immunization event created") +def TriggerSearchGetRequest(context): + get_search_get_url_header(context) + context.params = convert_to_form_data( + set_request_data( + context.patient.identifier[0].value, context.vaccine_type, datetime.today().strftime("%Y-%m-%d") + ) + ) + print(f"\n Search Get Parameters - \n {context.params}") + context.response = http_requests_session.get(context.url, params=context.params, headers=context.headers) + + print(f"\n Search Get Response - \n {context.response.json()}") + + +@when("Send a search request with POST method for Immunization event created") +def TriggerSearchPostRequest(context): + get_search_post_url_header(context) + context.request = convert_to_form_data( + set_request_data( + context.patient.identifier[0].value, context.vaccine_type, datetime.today().strftime("%Y-%m-%d") + ) + ) + print(f"\n Search Post Request - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + print(f"\n Search Post Response - \n {context.response.json()}") + + +@when( + parsers.parse( + "Send a search request with GET method with invalid NHS Number '{NHSNumber}' and valid Disease Type '{DiseaseType}'" + ) +) +@when( + parsers.parse( + "Send a search request with GET method with valid NHS Number '{NHSNumber}' and invalid Disease Type '{DiseaseType}'" + ) +) +@when( + parsers.parse( + "Send a search request with GET method with invalid NHS Number '{NHSNumber}' and invalid Disease Type '{DiseaseType}'" + ) +) +def send_invalid_param_get_request(context, NHSNumber, DiseaseType): + get_search_get_url_header(context) + + NHSNumber = normalize_param(NHSNumber) + DiseaseType = normalize_param(DiseaseType) + + context.params = convert_to_form_data( + set_request_data(NHSNumber, DiseaseType, datetime.today().strftime("%Y-%m-%d")) + ) + print(f"\n Search Get parameters - \n {context.params}") + context.response = http_requests_session.get(context.url, params=context.params, headers=context.headers) + + +@when( + parsers.parse( + "Send a search request with POST method with invalid NHS Number '{NHSNumber}' and valid Disease Type '{DiseaseType}'" + ) +) +@when( + parsers.parse( + "Send a search request with POST method with valid NHS Number '{NHSNumber}' and invalid Disease Type '{DiseaseType}'" + ) +) +@when( + parsers.parse( + "Send a search request with POST method with invalid NHS Number '{NHSNumber}' and invalid Disease Type '{DiseaseType}'" + ) +) +def send_invalid_param_post_request(context, NHSNumber, DiseaseType): + get_search_post_url_header(context) + + NHSNumber = normalize_param(NHSNumber) + DiseaseType = normalize_param(DiseaseType) + + context.request = convert_to_form_data( + set_request_data(NHSNumber, DiseaseType, datetime.today().strftime("%Y-%m-%d")) + ) + print(f"\n Search Post request - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when( + parsers.parse( + "Send a search request with GET method with invalid Date From '{DateFrom}' and valid Date To '{DateTo}'" + ) +) +@when( + parsers.parse( + "Send a search request with GET method with valid Date From '{DateFrom}' and invalid Date To '{DateTo}'" + ) +) +@when( + parsers.parse( + "Send a search request with GET method with invalid Date From '{DateFrom}' and invalid Date To '{DateTo}'" + ) +) +def send_invalid_date_get_request(context, DateFrom, DateTo): + get_search_get_url_header(context) + + # DateFrom = normalize_param(DateFrom.lower()) + # DateTo = normalize_param(DateTo.lower()) + + context.params = convert_to_form_data(set_request_data(9001066569, context.vaccine_type, DateFrom, DateTo)) + print(f"\n Search Get parameters - \n {context.params}") + context.response = http_requests_session.get(context.url, params=context.params, headers=context.headers) + + +@when( + parsers.parse( + "Send a search request with POST method with invalid Date From '{DateFrom}' and valid Date To '{DateTo}'" + ) +) +@when( + parsers.parse( + "Send a search request with POST method with valid Date From '{DateFrom}' and invalid Date To '{DateTo}'" + ) +) +@when( + parsers.parse( + "Send a search request with POST method with invalid Date From '{DateFrom}' and invalid Date To '{DateTo}'" + ) +) +def send_invalid_param_post_request_with_dates(context, DateFrom, DateTo): + get_search_post_url_header(context) + + # DateFrom = normalize_param(DateFrom.lower()) + # DateTo = normalize_param(DateTo) + + context.request = convert_to_form_data(set_request_data(9001066569, context.vaccine_type, DateFrom, DateTo)) + print(f"\n Search Post request - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when( + parsers.parse( + "Send a search request with GET method with valid NHS Number '{NHSNumber}' and Disease Type '{vaccine_type}' and Date From '{DateFrom}' and Date To '{DateTo}'" + ) +) +def send_valid_param_get_request(context, NHSNumber, vaccine_type, DateFrom, DateTo): + get_search_get_url_header(context) + + context.params = convert_to_form_data(set_request_data(NHSNumber, vaccine_type, DateFrom, DateTo)) + print(f"\n Search Get parameters - \n {context.params}") + context.response = http_requests_session.get(context.url, params=context.params, headers=context.headers) + + +@when( + parsers.parse( + "Send a search request with POST method with valid NHS Number '{NHSNumber}' and Disease Type '{vaccine_type}' and Date From '{DateFrom}' and Date To '{DateTo}'" + ) +) +def send_valid_param_post_request(context, NHSNumber, vaccine_type, DateFrom, DateTo): + get_search_post_url_header(context) + + context.request = convert_to_form_data(set_request_data(NHSNumber, vaccine_type, DateFrom, DateTo)) + print(f"\n Search Get parameters - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when( + parsers.parse( + "Send a search request with GET method with valid NHS Number '{NHSNumber}' and valid Disease Type '{vaccine_type}' and invalid include '{include}'" + ) +) +def send_valid_param_get_request_with_include(context, NHSNumber, vaccine_type, include): + get_search_get_url_header(context) + context.params = convert_to_form_data(set_request_data(NHSNumber, vaccine_type, include=include)) + print(f"\n Search Get parameters - \n {context.params}") + context.response = http_requests_session.get(context.url, params=context.params, headers=context.headers) + + +@when( + parsers.parse( + "Send a search request with POST method with valid NHS Number '{NHSNumber}' and valid Disease Type '{vaccine_type}' and invalid include '{include}'" + ) +) +def send_valid_param_post_request_with_include(context, NHSNumber, vaccine_type, include): + get_search_post_url_header(context) + context.request = convert_to_form_data(set_request_data(NHSNumber, vaccine_type, include=include)) + print(f"\n Search Post parameters - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when( + parsers.parse( + "Send a search request with POST method with valid NHS Number '{NHSNumber}' and valid Disease Type '{vaccine_type}' and Date From '{DateFrom}' and Date To '{DateTo}' and include '{include}'" + ) +) +def send_valid_param_post_request_with_include_and_dates(context, NHSNumber, vaccine_type, DateFrom, DateTo, include): + get_search_post_url_header(context) + context.request = convert_to_form_data(set_request_data(NHSNumber, vaccine_type, DateFrom, DateTo, include)) + print(f"\n Search Post parameters - \n {context.request}") + context.response = http_requests_session.post(context.url, headers=context.headers, data=context.request) + + +@when( + parsers.parse( + "Send a search request with GET method with valid NHS Number '{NHSNumber}' and valid Disease Type '{vaccine_type}' and Date From '{DateFrom}' and Date To '{DateTo}' and include '{include}'" + ) +) +def send_valid_param_get_request_with_include_and_dates(context, NHSNumber, vaccine_type, DateFrom, DateTo, include): + get_search_get_url_header(context) + context.params = convert_to_form_data(set_request_data(NHSNumber, vaccine_type, DateFrom, DateTo, include)) + print(f"\n Search Get parameters - \n {context.params}") + context.response = http_requests_session.get(context.url, params=context.params, headers=context.headers) + + +@then("The occurrenceDateTime of the immunization events should be within the Date From and Date To range") +def validateDateRange(context): + data = context.response.json() + context.parsed_search_object = parse_FHIR_immunization_response(data) + + params = getattr(context, "params", getattr(context, "request", {})) + + if isinstance(params, str): + parsed = parse_qs(params) + params = {k: v[0] for k, v in parsed.items()} if parsed else {} + + dateFrom = params.get("-date.from") + dateTo = params.get("-date.to") + + assert context.parsed_search_object.entry, "No entries found in the search response." + + for entry in context.parsed_search_object.entry: + if entry.resource.resourceType == "Immunization": + occurrence_date = entry.resource.occurrenceDateTime + id = entry.resource.id + if occurrence_date: + if dateFrom and dateTo: + occurrence_date = iso_to_compact(occurrence_date) + date_from = iso_to_compact(dateFrom) + date_to = iso_to_compact(dateTo) + + assert date_from <= occurrence_date <= date_to, ( + f"Occurrence date {occurrence_date} is not within the range Date From {context.DateFrom} and Date To {context.DateTo}. Imms ID: {id}" + ) + + +@then("The Search Response JSONs should contain the detail of the immunization events created above") +def validateImmsID(context): + data = context.response.json() + context.parsed_search_object = parse_FHIR_immunization_response(data) + + assert context.parsed_search_object.resourceType == "Bundle", ( + f"expected resourceType to be 'Bundle' but got {context.parsed_search_object.resourceType}" + ) + assert context.parsed_search_object.type == "searchset", ( + f"expected resourceType to be 'searchset' but got {context.parsed_search_object.type}" + ) + assert context.parsed_search_object.link[0].relation == "self", ( + f"expected link relation to be 'self' but got {context.parsed_search_object.link[0].relation}" + ) + assert context.parsed_search_object.link[0].url.startswith(context.baseUrl), ( + f"Expected link URL to start with '{context.baseUrl}', but got '{context.parsed_search_object.link[0].url}'" + ) + assert context.parsed_search_object.total >= 1, ( + f"expected total to be greater than or equal to 1 but got {context.parsed_search_object.total}" + ) + + context.created_event = find_entry_by_Imms_id(context.parsed_search_object, context.ImmsID) + + if context.created_event is None: + raise AssertionError(f"No object found with Immunisation ID {context.ImmsID} in the search response.") + + patient_reference = getattr(context.created_event.resource.patient, "reference", None) + + if not patient_reference: + raise ValueError("Patient reference is missing in the found event.") + + # Assign to context for further usage + context.Patient_fullUrl = patient_reference + + +@then( + "The Search Response JSONs field values should match with the input JSONs field values for resourceType Immunization" +) +def validateJsonImms(context): + create_obj = context.create_object + created_event = context.created_event.resource + validate_to_compare_request_and_response(context, create_obj, created_event) + + +@then("The Search Response JSONs field values should match with the input JSONs field values for resourceType Patient") +def validateJsonPat(context): + response_patient_entry = find_patient_by_fullurl(context.parsed_search_object) + assert response_patient_entry is not None, f"No Patient found with fullUrl {context.Patient_fullUrl}" + + response_patient = response_patient_entry.resource + expected_nhs_number = context.create_object.contained[1].identifier[0].value + actual_nhs_number = response_patient.identifier[0].value + expected_system = context.create_object.contained[1].identifier[0].system + actual_system = response_patient.identifier[0].system + + fields_to_compare = [ + ("fullUrl", context.Patient_fullUrl, response_patient_entry.fullUrl), + ("resourceType", "Patient", response_patient.resourceType), + ("id", expected_nhs_number, response_patient.id), + ("identifier.system", expected_system, actual_system), + ("identifier.value", expected_nhs_number, actual_nhs_number), + ] + + for name, expected, actual in fields_to_compare: + check.is_true(expected == actual, f"Expected {name}: {expected}, Actual {actual}") + + +@then("correct immunization event is returned in the response") +def validate_correct_immunization_event(context): + data = context.response.json() + context.parsed_search_object = parse_FHIR_immunization_response(data) + + context.created_event = context.parsed_search_object.entry[0] if context.parsed_search_object.entry else None + + if context.created_event is None: + raise AssertionError(f"No object found with Immunisation ID {context.ImmsID} in the search response.") + + validateJsonImms(context) + + assert context.parsed_search_object.resourceType == "Bundle", ( + f"expected resourceType to be 'Bundle' but got {context.parsed_search_object.resourceType}" + ) + assert context.parsed_search_object.type == "searchset", ( + f"expected resourceType to be 'searchset' but got {context.parsed_search_object.type}" + ) + assert context.parsed_search_object.link[0].relation == "self", ( + f"expected link relation to be 'self' but got {context.parsed_search_object.link[0].relation}" + ) + assert context.parsed_search_object.link[0].url.startswith(context.baseUrl), ( + f"Expected link URL to start with '{context.baseUrl}', but got '{context.parsed_search_object.link[0].url}'" + ) + assert context.parsed_search_object.total == 1, ( + f"expected total to be greater than or equal to 1 but got {context.parsed_search_object.total}" + ) + + +@then("correct immunization event is returned in the response with only specified elements") +def validate_correct_immunization_event_with_elements(context): + response = context.response.json() + assert response.get("resourceType") == "Bundle", "resourceType should be 'Bundle'" + assert response.get("type") == "searchset", "type should be 'searchset'" + assert isinstance(response.get("entry"), list) and len(response["entry"]) > 0, " entry list is missing or empty" + + # Link validation + link = response.get("link", [{}])[0] + link_url = link.get("url") + assert link_url is not None, " link[0].url is missing" + assert link_url.startswith(context.baseUrl), f"link[0].url should start with '{context.baseUrl}', got '{link_url}'" + + # Entry resource validation + resource = response["entry"][0].get("resource", {}) + assert resource.get("resourceType") == "Immunization", "resourceType should be 'Immunization'" + assert "id" in resource, "resource.id is missing" + assert "meta" in resource and "versionId" in resource["meta"], " meta.versionId is missing" + + assert resource["id"] == context.ImmsID, f"resource.id mismatch: expected '{context.ImmsID}', got '{resource['id']}'" + assert str(resource["meta"]["versionId"]) == str(context.expected_version), ( + f"meta.versionId mismatch: expected '{context.expected_version}', got '{resource['meta']['versionId']}'" + ) + + assert response.get("total") == 1, "total should be 1" + + +@then("Empty immunization event is returned in the response") +def validate_empty_immunization_event(context): + response = context.response.json() + assert response.get("resourceType") == "Bundle", "resourceType should be 'Bundle'" + assert response.get("type") == "searchset", "type should be 'searchset'" + assert isinstance(response.get("entry"), list) and len(response["entry"]) == 0, " entry list should be empty" + + # Link validation + link = response.get("link", [{}])[0] + link_url = link.get("url") + assert link_url is not None, " link[0].url is missing" + assert link_url == f"{context.baseUrl}/Immunization?identifier=None", ( + f"link[0].url should be '{context.baseUrl}/Immunization?identifier=None', got '{link_url}'" + ) + + assert response.get("total") == 0, "total should be 0" diff --git a/tests/e2e_automation/features/APITests/steps/test_status_steps.py b/tests/e2e_automation/features/APITests/steps/test_status_steps.py new file mode 100644 index 0000000000..cd4d14d8b4 --- /dev/null +++ b/tests/e2e_automation/features/APITests/steps/test_status_steps.py @@ -0,0 +1,54 @@ +import os + +import pytest +import requests +from pytest_bdd import scenarios, then, when +from utilities.context import ScenarioContext +from utilities.http_requests_session import http_requests_session + +scenarios("APITests/status.feature") + +CONNECTION_ABORTED_ERROR_MSG = "" + + +@when("I send a request to the ping endpoint") +def send_request_to_ping_endpoint(context: ScenarioContext) -> None: + context.response = http_requests_session.get(context.baseUrl + "/_ping") + + +@when("I send a request to the status endpoint") +def send_request_status_endpoint(context: ScenarioContext) -> None: + # Let exception be raised if expected env var is not present + status_api_key: str = os.environ["STATUS_API_KEY"] + context.response = http_requests_session.get(context.baseUrl + "/_status", headers={"apikey": status_api_key}) + + +@when("I send a direct request to the AWS backend") +def send_request_to_aws_backend(context: ScenarioContext) -> None: + # Let exception be raised if expected env var is not present + aws_domain_name: str = os.environ["AWS_DOMAIN_NAME"] + backend_status_url = "https://" + aws_domain_name + "/status" + + with pytest.raises(requests.exceptions.ConnectionError) as e: + requests.get(backend_status_url) + + context.response = str(e) + + +@when("I send an unauthenticated request to the API") +def send_unauthenticated_request_to_api(context: ScenarioContext) -> None: + context.response = http_requests_session.get(context.baseUrl + "/Immunization") + + +@then("The status response will contain a passing healthcheck") +def check_status_response_healthy(context: ScenarioContext) -> None: + status_response = context.response.json() + assertion_failure_msg = f"Status response assertions failed. Res: {status_response}" + + assert status_response.get("status") == "pass", assertion_failure_msg + assert status_response.get("checks", {}).get("healthcheck", {}).get("status") == "pass", assertion_failure_msg + + +@then("The request is rejected") +def check_the_direct_req_is_rejected(context: ScenarioContext) -> None: + assert context.response == CONNECTION_ABORTED_ERROR_MSG, f"got unexpected mTLS error msg: {context.response}" diff --git a/tests/e2e_automation/features/APITests/steps/test_update_steps.py b/tests/e2e_automation/features/APITests/steps/test_update_steps.py new file mode 100644 index 0000000000..b607e2d869 --- /dev/null +++ b/tests/e2e_automation/features/APITests/steps/test_update_steps.py @@ -0,0 +1,104 @@ +import uuid + +from pytest_bdd import parsers, scenarios, then, when +from src.dynamoDB.dynamo_db_helper import ( + fetch_immunization_int_delta_detail_by_immsID, + validate_imms_delta_record_with_created_event, +) +from src.objectModels.api_immunization_builder import convert_to_update +from utilities.api_fhir_immunization_helper import parse_error_response, validate_error_response +from utilities.api_get_header import get_update_url_header +from utilities.date_helper import generate_date +from utilities.enums import ActionFlag, Operation + +from .common_steps import ( + send_update_for_immunization_event, + trigger_the_updated_request, + valid_json_payload_is_created, + valid_token_is_generated, +) + +scenarios("APITests/update.feature") + + +@when(parsers.parse("Send a update for Immunization event created with patient address being updated by '{Supplier}'")) +def send_update_for_immunization_event_by_supplier(context, Supplier): + valid_token_is_generated(context, Supplier) + send_update_for_immunization_event(context) + + +@then("The delta table will be populated with the correct data for updated event") +def validate_delta_table_for_updated_event(context): + create_obj = context.create_object + items = fetch_immunization_int_delta_detail_by_immsID( + context.aws_profile_name, context.ImmsID, context.S3_env, context.expected_version + ) + assert items, f"Items not found in response for ImmsID: {context.ImmsID}" + delta_items = [i for i in items if i.get("Operation") == Operation.updated.value] + assert delta_items, f"No item found for ImmsID: {context.ImmsID}" + item = [max(delta_items, key=lambda x: x.get("DateTimeStamp", 0))] + validate_imms_delta_record_with_created_event( + context, create_obj, item, Operation.updated.value, ActionFlag.updated.value + ) + + +@when( + parsers.parse("Send a update for Immunization event created with occurrenceDateTime being updated to '{DateText}'") +) +def send_update_for_immunization_event_with_occurrenceDateTime(context, DateText): + get_update_url_header(context, str(context.expected_version)) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + context.update_object.occurrenceDateTime = generate_date(DateText) + trigger_the_updated_request(context) + + +@when(parsers.parse("Send a update for Immunization event created with recorded being updated to '{DateText}'")) +def send_update_for_immunization_event_with_recorded_date_update(context, DateText): + get_update_url_header(context, str(context.expected_version)) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + context.update_object.recorded = generate_date(DateText) + trigger_the_updated_request(context) + + +@when( + parsers.parse("Send a update for Immunization event created with patient date of bith being updated to '{DateText}'") +) +def send_update_for_immunization_event_with_dob_update(context, DateText): + get_update_url_header(context, str(context.expected_version)) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + context.update_object.contained[1].birthDate = generate_date(DateText) + trigger_the_updated_request(context) + + +@when(parsers.parse("Send a update for Immunization event created with expiration date being updated to '{DateText}'")) +def send_update_for_immunization_event_with_expiration_date_update(context, DateText): + get_update_url_header(context, str(context.expected_version)) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + context.update_object.expirationDate = generate_date(DateText) + trigger_the_updated_request(context) + + +@when("Send an update request for invalid immunization id") +def send_update_request_for_invalid_immunization_id(context): + valid_json_payload_is_created(context) + context.ImmsID = str(uuid.uuid4()) + get_update_url_header(context, str(context.expected_version)) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + trigger_the_updated_request(context) + + +@when(parsers.parse("Send an update request for invalid Etag {Etag}")) +def send_update_request_for_invalid_etag(context, Etag): + valid_json_payload_is_created(context) + context.ImmsID = str(uuid.uuid4()) + context.version = Etag + get_update_url_header(context, Etag) + context.update_object = convert_to_update(context.immunization_object, context.ImmsID) + trigger_the_updated_request(context) + + +@then(parsers.parse("The Response JSONs should contain correct error message for etag '{errorName}'")) +def validateForbiddenAccess(context, errorName): + error_response = parse_error_response(context.response.json()) + validate_error_response(error_response, errorName, version=context.version) + print(f"\n Error Response - \n {error_response}") diff --git a/tests/e2e_automation/features/APITests/update.feature b/tests/e2e_automation/features/APITests/update.feature new file mode 100644 index 0000000000..c73bd4095a --- /dev/null +++ b/tests/e2e_automation/features/APITests/update.feature @@ -0,0 +1,135 @@ +@Update_Feature @functional +Feature: Update the immunization of a patient + +@smoke +@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the Update API will be successful with all the valid parameters + Given I have created a valid vaccination record + When Send a update for Immunization event created with patient address being updated + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The imms event table will be populated with the correct data for 'updated' event + And The delta table will be populated with the correct data for updated event + + +@vaccine_type_RSV @patient_id_Random +Scenario: Verify that the updated event request will fail with forbidden access for MAVIS supplier + Given valid vaccination record is created by 'RAVS' supplier + When Send a update for Immunization event created with patient address being updated by 'MAVIS' + Then The request will be successful with the status code '403' + And The Response JSONs should contain correct error message for 'forbidden' access + + +@delete_cleanup @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: verify that vaccination record can be updated with valid vaccination detail + Given I have created a valid vaccination record + When Send a update for Immunization event created with vaccination detail being updated + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The imms event table will be populated with the correct data for 'updated' event + And The delta table will be populated with the correct data for updated event + + +@smoke +@Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth +Scenario: Flu event is created and updated twice + Given I have created a valid vaccination record + When Send a update for Immunization event created with patient address being updated + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The imms event table will be populated with the correct data for 'updated' event + And The delta table will be populated with the correct data for updated event + When Send a update for Immunization event created with vaccination detail being updated + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The imms event table will be populated with the correct data for 'updated' event + And The delta table will be populated with the correct data for updated event + +@vaccine_type_FLU @patient_id_Random +Scenario: Verify that update will be successful when request is triggered by other supplier with authorize permission + Given valid vaccination record is created by 'Postman_Auth' supplier + When Send a update for Immunization event created with patient address being updated by 'MAVIS' + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The imms event table will be populated with the correct data for 'updated' event + And The delta table will be populated with the correct data for updated event + +@Delete_cleanUp @vaccine_type_RSV @patient_id_Mod11_NHS @supplier_name_Postman_Auth +Scenario: Verify that the Update API will be successful with invalid but Mod11 compliant NHS Number + Given I have created a valid vaccination record + When Send a update for Immunization event created with patient address being updated + Then The request will be successful with the status code '200' + And The Etag in header will containing the latest event version + And The X-Request-ID and X-Correlation-ID keys in header will populate correctly + And The imms event table will be populated with the correct data for 'updated' event + And The delta table will be populated with the correct data for updated event + +@Delete_cleanUp @vaccine_type_RSV @patient_id_Mod11_NHS @supplier_name_Postman_Auth +Scenario Outline: Scenario Outline name: Verify that the Update API will be fails if occurrenceDateTime has future or invalid formatted date + Given I have created a valid vaccination record + When Send a update for Immunization event created with occurrenceDateTime being updated to '' + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for 'invalid_OccurrenceDateTime' + Examples: + | Date | + | future_occurrence | + | invalid_format | + | nonexistent | + | empty | + +@Delete_cleanUp @vaccine_type_RSV @patient_id_Mod11_NHS @supplier_name_Postman_Auth +Scenario Outline: Scenario Outline name: Verify that the Update API will be fails if recorded has future or invalid formatted date + Given I have created a valid vaccination record + When Send a update for Immunization event created with recorded being updated to '' + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for 'invalid_recorded' + Examples: + | Date | + | future_date | + | invalid_format | + | nonexistent | + | empty | + +@Delete_cleanUp @vaccine_type_RSV @patient_id_Mod11_NHS @supplier_name_Postman_Auth +Scenario Outline: Scenario Outline name: Verify that the Update API will be fails if expiration date has invalid formatted date + Given I have created a valid vaccination record + When Send a update for Immunization event created with expiration date being updated to '' + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for 'invalid_expirationDate' + Examples: + | Date | + | invalid_format | + | nonexistent | + | empty | + +@Delete_cleanUp @vaccine_type_RSV @patient_id_Mod11_NHS @supplier_name_Postman_Auth +Scenario Outline: Verify that the Update API will be fails if patient's date of birth has future or invalid formatted date + Given I have created a valid vaccination record + When Send a update for Immunization event created with patient date of bith being updated to '' + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for '' + Examples: + | Date | error_type | + | future_date | future_DateOfBirth | + | invalid_format | invalid_DateOfBirth | + | nonexistent | invalid_DateOfBirth | + | empty | invalid_DateOfBirth | + +@vaccine_type_3IN1 @patient_id_Random @supplier_name_Postman_Auth +Scenario: Verify that the update request will fail for invalid immunization id + When Send an update request for invalid immunization id + Then The request will be unsuccessful with the status code '404' + And The Response JSONs should contain correct error message for 'not_found' + +@vaccine_type_3IN1 @patient_id_Random @supplier_name_Postman_Auth +Scenario Outline: Verify that the update request will fail for invalid Etag value + When Send an update request for invalid Etag + Then The request will be unsuccessful with the status code '400' + And The Response JSONs should contain correct error message for etag 'invalid_etag' + Examples: + | Etag | + | 0 | + | -1 | + | abcde | diff --git a/tests/e2e/utils/__init__.py b/tests/e2e_automation/features/__init__.py similarity index 100% rename from tests/e2e/utils/__init__.py rename to tests/e2e_automation/features/__init__.py diff --git a/tests/e2e_automation/features/batchTests/Steps/__init__.py b/tests/e2e_automation/features/batchTests/Steps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_automation/features/batchTests/Steps/batch_common_steps.py b/tests/e2e_automation/features/batchTests/Steps/batch_common_steps.py new file mode 100644 index 0000000000..95f06f65ed --- /dev/null +++ b/tests/e2e_automation/features/batchTests/Steps/batch_common_steps.py @@ -0,0 +1,342 @@ +import functools +import json +import os +import re +from datetime import datetime, timezone + +import pandas as pd +import pytest_check as check +from pytest_bdd import given, parsers, then, when +from src.dynamoDB.dynamo_db_helper import ( + fetch_batch_audit_table_detail, + fetch_immunization_events_detail_by_IdentifierPK, + fetch_immunization_int_delta_detail_by_immsID, + parse_imms_int_imms_event_response, + validate_audit_table_record, + validate_imms_delta_record_with_batch_record, + validate_to_compare_batch_record_with_event_table_record, +) +from src.objectModels.batch.batch_file_builder import ( + build_batch_file, + generate_file_name, + save_record_to_batch_files_directory, +) +from utilities.batch_file_helper import ( + read_and_validate_bus_ack_file_content, + validate_bus_ack_file_for_error, + validate_bus_ack_file_for_successful_records, + validate_inf_ack_file, +) +from utilities.batch_S3_buckets import upload_file_to_S3, wait_and_read_ack_file, wait_for_file_to_move_archive +from utilities.enums import ActionFlag, ActionMap, Operation + + +def ignore_if_local_run(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Extract context from args or kwargs + context = kwargs.get("context") if "context" in kwargs else (args[-1] if args else None) + + if context and getattr(context, "LOCAL_RUN_WITHOUT_S3_UPLOAD", False): + print(f"Skipping step '{func.__name__}' due to local execution mode.") + return None + return func(*args, **kwargs) + + return wrapper + + +def ignore_local_run_set_test_data(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Extract context from args or kwargs + context = kwargs.get("context") if "context" in kwargs else (args[-1] if args else None) + + if context and getattr(context, "LOCAL_RUN_WITHOUT_S3_UPLOAD", False): + print(f"Skipping step '{func.__name__}' due to local execution mode.") + + file_name = os.getenv("LOCAL_RUN_FILE_NAME") + context.filename = file_name + file_path = os.path.join(context.working_directory, file_name) + + # Read file into vaccine_df + try: + context.vaccine_df = pd.read_csv( + file_path, + delimiter="|", # or "," depending on your export logic + quotechar='"', + dtype=str, # optional: ensures all columns are read as strings + ) + print(f"Loaded fallback vaccine_df from {file_name}") + except Exception as e: + print(f"Failed to load fallback file {file_name}: {e}") + context.vaccine_df = pd.DataFrame() # fallback to empty + + return None + + return func(*args, **kwargs) + + return wrapper + + +@given("batch file is created for below data as full dataset") +@ignore_if_local_run +def valid_batch_file_is_created_with_details(datatable, context): + build_dataFrame_using_datatable(datatable, context) + create_batch_file(context) + + +@when("same batch file is uploaded again in s3 bucket") +@when("batch file is uploaded in s3 bucket") +@ignore_local_run_set_test_data +def batch_file_upload_in_s3_bucket(context): + upload_file_to_S3(context) + print(f"Batch file uploaded to S3: {context.filename}") + fileIsMoved = wait_for_file_to_move_archive(context) + assert fileIsMoved, "File not found in archive after timeout" + + +@then("file will be moved to destination bucket and inf ack file will be created") +def ack_file_will_be_moved_to_destination_bucket(context): + context.fileContent = wait_and_read_ack_file(context, "ack") + assert context.fileContent, f"File not found in destination bucket after timeout: {context.forwarded_prefix}" + + +@then("inf ack file has success status for processed batch file") +def all_records_are_processed_successfully_in_the_inf_ack_file(context): + all_valid = validate_inf_ack_file(context) + assert all_valid, "One or more records failed validation checks" + + +@then("bus ack file will be created") +def file_will_be_moved_to_destination_bucket(context): + context.fileContent = wait_and_read_ack_file(context, "forwardedFile") + assert context.fileContent, f"File not found in destination bucket after timeout: {context.forwarded_prefix}" + + +@then("bus ack will not have any entry of successfully processed records") +def all_records_are_processed_successfully_in_the_batch_file(context): + file_rows = read_and_validate_bus_ack_file_content(context) + all_valid = validate_bus_ack_file_for_successful_records(context, file_rows) + assert all_valid, "One or more records failed validation checks" + + +@then("Audit table will have correct status, queue name and record count for the processed batch file") +def validate_imms_audit_table(context): + table_query_response = fetch_batch_audit_table_detail(context.aws_profile_name, context.filename, context.S3_env) + + assert isinstance(table_query_response, list) and table_query_response, ( + f"Item not found in response for filename: {context.filename}" + ) + item = table_query_response[0] + validate_audit_table_record(context, item, "Processed") + + +@then("The delta table will be populated with the correct data for all created records in batch file") +def validate_imms_delta_table_for_created_records_in_batch_file(context): + preload_delta_data(context) + validate_imms_delta_table_for_newly_created_records_in_batch_file(context) + + +@then("The delta table will be populated with the correct data for all updated records in batch file") +def validate_imms_delta_table_for_updated_records(context): + if context.delta_cache is None: + preload_delta_data(context) + validate_imms_delta_table_for_updated_records_in_batch_file(context) + + +@then("The delta table will be populated with the correct data for all deleted records in batch file") +def validate_imms_delta_table_for_deleted_records(context): + if context.delta_cache is None: + preload_delta_data(context) + validate_imms_delta_table_for_deleted_records_in_batch_file(context) + + +@then( + parsers.parse( + "The imms event table will be populated with the correct data for '{operation}' event for records in batch file" + ) +) +def validate_imms_event_table_for_all_records_in_batch_file(context, operation: Operation): + mapping = ActionMap[operation.lower()] + df = context.vaccine_df[context.vaccine_df["ACTION_FLAG"].str.lower() == mapping.action_flag.value.lower()] + + df["UNIQUE_ID_COMBINED"] = df["UNIQUE_ID_URI"].astype(str) + "#" + df["UNIQUE_ID"].astype(str) + valid_rows = df[df["UNIQUE_ID_COMBINED"].notnull() & (df["UNIQUE_ID_COMBINED"] != "nan#nan")] + + for idx, row in valid_rows.iterrows(): + unique_id_combined = row["UNIQUE_ID_COMBINED"] + batch_record = {k: normalize(v) for k, v in row.to_dict().items()} + + table_query_response = fetch_immunization_events_detail_by_IdentifierPK( + context.aws_profile_name, unique_id_combined, context.S3_env + ) + assert "Items" in table_query_response and table_query_response["Count"] > 0, ( + f"Item not found in response for unique_id_combined: {unique_id_combined}" + ) + + item = table_query_response["Items"][0] + + df.at[idx, "IMMS_ID"] = item.get("PK") + context.ImmsID = item.get("PK").replace("Immunization#", "") + update_imms_id_for_all_related_rows(context.vaccine_df, unique_id_combined, context.ImmsID) + + resource_json_str = item.get("Resource") + assert resource_json_str, "Resource field missing in item." + + try: + resource = json.loads(resource_json_str) + except (TypeError, json.JSONDecodeError) as e: + print(f"Failed to parse Resource from item: {e}") + raise AssertionError("Failed to parse Resource from response item.") + + assert resource is not None, "Resource is None in the response" + created_event = parse_imms_int_imms_event_response(resource) + + nhs_number = batch_record.get("NHS_NUMBER") or "TBC" + + fields_to_compare = [ + ("Operation", Operation[operation].value, item.get("Operation")), + ("SupplierSystem", context.supplier_name, item.get("SupplierSystem")), + ("PatientPK", f"Patient#{nhs_number}", item.get("PatientPK")), + ("PatientSK", f"{context.vaccine_type.upper()}#{context.ImmsID}", item.get("PatientSK")), + ("Version", int(context.expected_version), int(item.get("Version"))), + ] + + for name, expected, actual in fields_to_compare: + check.is_true(expected == actual, f"Expected {name}: {expected}, Actual {actual}") + + validate_to_compare_batch_record_with_event_table_record(context, batch_record, created_event) + + +@then("all records are rejected in the bus ack file and no imms id is generated") +def all_record_are_rejected_for_given_field_name(context): + file_rows = read_and_validate_bus_ack_file_content(context) + all_valid = validate_bus_ack_file_for_error(context, file_rows) + assert all_valid, "One or more records failed validation checks" + + +def normalize(value): + return "" if pd.isna(value) or value == "" else value + + +def create_batch_file(context, file_ext: str = "csv", fileName: str = None, delimiter: str = "|"): + offset = datetime.now().astimezone().strftime("%z")[-2:] + context.FileTimestamp = datetime.now().astimezone().strftime("%Y%m%dT%H%M%S") + offset + context.file_extension = file_ext + + timestamp_pattern = r"\d{8}T\d{8}" + + if not fileName: + context.filename = generate_file_name(context) + else: + suffix = "" if re.search(timestamp_pattern, fileName) else f"_{context.FileTimestamp}" + context.filename = f"{fileName}{suffix}.{context.file_extension}" + + save_record_to_batch_files_directory(context, delimiter) + + print(f"Batch file created: {context.filename}") + + +def build_dataFrame_using_datatable(datatable, context): + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S") + headers = datatable[0] + rows = datatable[1:] + + table_list = [(row[headers.index("patient_id")], f"{row[headers.index('unique_id')]}-{timestamp}") for row in rows] + records = [] + for patient_id, unique_id in table_list: + context.patient_id = patient_id + record = build_batch_file(context, unique_id=unique_id) + flat_record = record.dict() + if "data" in flat_record: + flat_record = flat_record["data"] + records.append(flat_record) + + context.vaccine_df = pd.DataFrame(records) + + +def update_imms_id_for_all_related_rows(df, unique_id_combined, imms_id): + mask = (df["UNIQUE_ID_URI"].astype(str) + "#" + df["UNIQUE_ID"].astype(str)) == unique_id_combined + df.loc[mask, "IMMS_ID"] = imms_id + + +def preload_delta_data(context): + df = context.vaccine_df + + check.is_true("IMMS_ID" in df.columns, "Column 'IMMS_ID' not found in vaccine_df") + + valid_rows = df[df["IMMS_ID"].notnull()] + check.is_true(not valid_rows.empty, "No rows with non-null IMMS_ID found in vaccine_df") + + grouped = valid_rows.groupby("IMMS_ID") + + context.delta_cache = {} + + for imms_id, group in grouped: + clean_id = imms_id.replace("Immunization#", "") + delta_items = fetch_immunization_int_delta_detail_by_immsID(context.aws_profile_name, clean_id, context.S3_env) + check.is_true(delta_items, f"No delta records returned for IMMS_ID: {clean_id}") + + context.delta_cache[clean_id] = {"rows": group, "delta_items": delta_items} + + +def validate_imms_delta_table_for_newly_created_records_in_batch_file(context): + for clean_id, data in context.delta_cache.items(): + rows = data["rows"] + delta_items = data["delta_items"] + + create_items = [i for i in delta_items if i.get("Operation") == "CREATE"] + + check.is_true( + len(create_items) == 1, f"Expected exactly 1 CREATE record for IMMS_ID {clean_id}, found {len(create_items)}" + ) + + create_item = create_items[0] + + for _, row in rows[rows["ACTION_FLAG"] == "NEW"].iterrows(): + batch_record = {k: normalize(v) for k, v in row.to_dict().items()} + + validate_imms_delta_record_with_batch_record( + context, batch_record, create_item, Operation.created.value, ActionFlag.created.value + ) + + +def validate_imms_delta_table_for_updated_records_in_batch_file(context): + for clean_id, data in context.delta_cache.items(): + rows = data["rows"] + delta_items = data["delta_items"] + + update_items = [i for i in delta_items if i.get("Operation") == "UPDATE"] + check.is_true(update_items, f"No UPDATE records for IMMS_ID {clean_id}") + updated_index = context.expected_version - 2 + for _, row in rows[rows["ACTION_FLAG"] == "UPDATE"].iterrows(): + batch_record = {k: normalize(v) for k, v in row.to_dict().items()} + item = update_items.pop(updated_index) + + validate_imms_delta_record_with_batch_record( + context, batch_record, item, Operation.updated.value, ActionFlag.updated.value + ) + + +def validate_imms_delta_table_for_deleted_records_in_batch_file(context): + for clean_id, data in context.delta_cache.items(): + rows = data["rows"] + delta_items = data["delta_items"] + + delete_item = next((i for i in delta_items if i.get("Operation") == "DELETE"), None) + + check.is_true(delete_item, f"No DELETE record for IMMS_ID {clean_id}") + + delete_rows = rows[rows["ACTION_FLAG"] == "DELETE"] + + check.is_true( + len(delete_rows) == 1, + f"Expected exactly 1 DELETE row in batch file for IMMS_ID {clean_id}, found {len(delete_rows)}", + ) + + row = delete_rows.iloc[0] + batch_record = {k: normalize(v) for k, v in row.to_dict().items()} + + validate_imms_delta_record_with_batch_record( + context, batch_record, delete_item, Operation.deleted.value, ActionFlag.deleted.value + ) diff --git a/tests/e2e_automation/features/batchTests/Steps/test_batch_file_validation_steps.py b/tests/e2e_automation/features/batchTests/Steps/test_batch_file_validation_steps.py new file mode 100644 index 0000000000..5ad954795c --- /dev/null +++ b/tests/e2e_automation/features/batchTests/Steps/test_batch_file_validation_steps.py @@ -0,0 +1,106 @@ +import pandas as pd +from pytest_bdd import given, parsers, scenarios, then +from src.dynamoDB.dynamo_db_helper import ( + fetch_batch_audit_table_detail, + update_audit_table_for_failed_status, + validate_audit_table_record, +) +from src.objectModels.batch.batch_file_builder import BatchVaccinationRecord +from utilities.batch_file_helper import validate_inf_ack_file +from utilities.batch_S3_buckets import wait_and_read_ack_file + +from .batch_common_steps import build_dataFrame_using_datatable, create_batch_file, ignore_if_local_run + +scenarios("batchTests/batch_file_validation.feature") + + +@given( + parsers.parse("batch file is created for below data with {invalid_filename} filename and {file_extension} extension") +) +@ignore_if_local_run +def valid_batch_file_is_created_with_details(datatable, context, invalid_filename, file_extension): + build_dataFrame_using_datatable(datatable, context) + create_batch_file(context, fileName=invalid_filename, file_ext=file_extension) + + +@given("Empty batch file is created") +@ignore_if_local_run +def empty_batch_file_is_created(context): + columns = list(BatchVaccinationRecord.__fields__.keys()) + context.vaccine_df = pd.DataFrame(columns=columns) + create_batch_file(context) + + +@given("batch file is created with missing columns for below data") +@ignore_if_local_run +def batch_file_with_missing_dob_is_created(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df = context.vaccine_df.drop(columns=["PERSON_DOB"]) + context.vaccine_df = context.vaccine_df.drop(columns=["PERFORMING_PROFESSIONAL_FORENAME"]) + create_batch_file(context) + + +@given("batch file is created with invalid column order for below data") +@ignore_if_local_run +def batch_file_with_invalid_column_order_is_created(datatable, context): + build_dataFrame_using_datatable(datatable, context) + columns = list(context.vaccine_df.columns) + columns[0], columns[1] = columns[1], columns[0] + context.vaccine_df = context.vaccine_df[columns] + create_batch_file(context) + + +@given("batch file is created with invalid delimiter for below data") +@ignore_if_local_run +def batch_file_with_invalid_delimiter_is_created(datatable, context): + build_dataFrame_using_datatable(datatable, context) + create_batch_file(context, delimiter=";") + + +@given("batch file is created with invalid column name for patient surname for below data") +@ignore_if_local_run +def batch_file_with_invalid_column_name_is_created(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df = context.vaccine_df.rename(columns={"PERSON_SURNAME": "PERSON_SURENAME"}) + create_batch_file(context) + + +@given("batch file is created with additional column person age for below data") +@ignore_if_local_run +def batch_file_with_additional_column_is_created(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df["PERSON_AGE"] = 30 + create_batch_file(context) + + +@then("file will be moved to destination bucket and inf ack file will be created for duplicate batch file upload") +def file_will_be_moved_to_destination_bucket(context): + context.fileContent = wait_and_read_ack_file(context, "ack", duplicate_inf_files=True) + assert context.fileContent, f"File not found in destination bucket after timeout: {context.forwarded_prefix}" + + +@then("inf ack file has failure status for processed batch file") +def failed_inf_ack_file(context): + all_valid = validate_inf_ack_file(context, success=False) + assert all_valid, "One or more records failed validation checks" + + +@then("bus ack file will not be created") +def file_will_not_be_moved_to_destination_bucket(context): + context.fileContent = wait_and_read_ack_file(context, "forwardedFile", timeout=10, duplicate_bus_files=True) + assert context.fileContent is None, f"File found in destination bucket: {context.forwarded_prefix}" + + +@then( + parsers.parse("Audit table will have '{status}', '{queue_name}' and '{error_details}' for the processed batch file") +) +def validate_imms_audit_table(context, status, queue_name, error_details): + table_query_response = fetch_batch_audit_table_detail(context.aws_profile_name, context.filename, context.S3_env) + + assert isinstance(table_query_response, list) and table_query_response, ( + f"Item not found in response for filename: {context.filename}" + ) + sorted_items = sorted(table_query_response, key=lambda x: x["timestamp"], reverse=True) + item = sorted_items[0] + validate_audit_table_record(context, item, status, error_details, queue_name) + update_audit_table_for_failed_status(item, context.aws_profile_name, context.S3_env) diff --git a/tests/e2e_automation/features/batchTests/Steps/test_create_batch_steps.py b/tests/e2e_automation/features/batchTests/Steps/test_create_batch_steps.py new file mode 100644 index 0000000000..57dca0955f --- /dev/null +++ b/tests/e2e_automation/features/batchTests/Steps/test_create_batch_steps.py @@ -0,0 +1,212 @@ +from pytest_bdd import given, scenarios +from src.objectModels.batch.batch_file_builder import get_batch_date +from utilities.text_helper import get_text + +from .batch_common_steps import build_dataFrame_using_datatable, create_batch_file, ignore_if_local_run + +scenarios("batchTests/create_batch.feature") + + +@given("batch file is created for below data as minimum dataset") +@ignore_if_local_run +def valid_batch_file_is_created_with_minimum_details(datatable, context): + build_dataFrame_using_datatable(datatable, context) + columns_to_clear = [ + "NHS_NUMBER", + "VACCINATION_PROCEDURE_TERM", + "VACCINE_PRODUCT_CODE", + "VACCINE_PRODUCT_TERM", + "VACCINE_MANUFACTURER", + "BATCH_NUMBER", + "SITE_OF_VACCINATION_CODE", + "SITE_OF_VACCINATION_TERM", + "EXPIRY_DATE", + "ROUTE_OF_VACCINATION_CODE", + "ROUTE_OF_VACCINATION_TERM", + "DOSE_SEQUENCE", + "DOSE_AMOUNT", + "DOSE_UNIT_CODE", + "DOSE_UNIT_TERM", + "INDICATION_CODE", + "PERFORMING_PROFESSIONAL_SURNAME", + "PERFORMING_PROFESSIONAL_FORENAME", + ] + context.vaccine_df.loc[:, columns_to_clear] = "" + create_batch_file(context) + + +@given("batch file is created for below data where date_and_time field has invalid date") +@ignore_if_local_run +def valid_batch_file_is_created_with_invalid_date_and_time(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df["DATE_AND_TIME"] = context.vaccine_df["UNIQUE_ID"].apply( + lambda uid: get_batch_date(uid.split("-")[1]) + ) + create_batch_file(context) + + +@given("batch file is created for below data where recorded field has invalid date") +@ignore_if_local_run +def valid_batch_file_is_created_with_invalid_recorded_date(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df["RECORDED_DATE"] = context.vaccine_df["UNIQUE_ID"].apply( + lambda uid: get_batch_date(uid.split("-")[1]) + ) + create_batch_file(context) + + +@given("batch file is created for below data where expiry field has invalid date") +@ignore_if_local_run +def valid_batch_file_is_created_with_invalid_expiry_date(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df["EXPIRY_DATE"] = context.vaccine_df["UNIQUE_ID"].apply( + lambda uid: get_batch_date(uid.split("-")[1]) + ) + create_batch_file(context) + + +@given("batch file is created for below data where Person date of birth field has invalid date") +@ignore_if_local_run +def valid_batch_file_is_created_with_invalid_person_dateOfBirth_date(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df["PERSON_DOB"] = context.vaccine_df["UNIQUE_ID"].apply( + lambda uid: get_batch_date(uid.split("-")[1]) + ) + create_batch_file(context) + + +@given("batch file is created for below data where Person detail has invalid data") +@ignore_if_local_run +def valid_batch_file_is_created_with_invalid_patient_data(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "NHS_NUMBER"] = "12345678" + context.vaccine_df.loc[1, "NHS_NUMBER"] = "1234567890" + context.vaccine_df.loc[2, "PERSON_FORENAME"] = "" + context.vaccine_df.loc[3, ["PERSON_FORENAME", "PERSON_SURNAME"]] = "" + context.vaccine_df.loc[4, "PERSON_SURNAME"] = "" + context.vaccine_df.loc[5, "PERSON_GENDER_CODE"] = "8" + context.vaccine_df.loc[6, "PERSON_GENDER_CODE"] = "unknow" + context.vaccine_df.loc[7, "PERSON_GENDER_CODE"] = "" + context.vaccine_df.loc[8, "PERSON_FORENAME"] = " " + context.vaccine_df.loc[9, "PERSON_SURNAME"] = " " + context.vaccine_df.loc[10, "PERSON_SURNAME"] = get_text("name_length_36") + context.vaccine_df.loc[11, "PERSON_FORENAME"] = get_text("name_length_181") + create_batch_file(context) + + +@given("batch file is created for below data where performer detail has invalid data") +@ignore_if_local_run +def valid_batch_file_is_created_with_invalid_performer_data(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "PERFORMING_PROFESSIONAL_FORENAME"] = "" + context.vaccine_df.loc[1, "PERFORMING_PROFESSIONAL_SURNAME"] = "" + create_batch_file(context) + + +@given("batch file is created for below data where person detail has valid values") +@ignore_if_local_run +def valid_batch_file_is_created_with_different_values_gender(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "PERSON_GENDER_CODE"] = "0" + context.vaccine_df.loc[1, "PERSON_GENDER_CODE"] = "1" + context.vaccine_df.loc[2, "PERSON_GENDER_CODE"] = "2" + context.vaccine_df.loc[3, "PERSON_GENDER_CODE"] = "9" + context.vaccine_df.loc[4, "PERSON_GENDER_CODE"] = "unknown" + context.vaccine_df.loc[5, "PERSON_GENDER_CODE"] = "male" + context.vaccine_df.loc[6, "PERSON_GENDER_CODE"] = "female" + context.vaccine_df.loc[7, "PERSON_GENDER_CODE"] = "other" + context.vaccine_df.loc[8, "PERSON_SURNAME"] = get_text("name_length_35") + context.vaccine_df.loc[9, "PERSON_FORENAME"] = get_text("name_length_35") + context.vaccine_df.loc[10, "PERSON_FORENAME"] = f"Elan {get_text('name_length_15')}" + create_batch_file(context) + + +@given( + "batch file is created for below data where mandatory fields for site, location, action flag, primary source and unique identifiers are missing" +) +@ignore_if_local_run +def valid_batch_file_is_created_with_missing_mandatory_fields(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "SITE_CODE"] = "" + context.vaccine_df.loc[1, "SITE_CODE_TYPE_URI"] = "" + context.vaccine_df.loc[2, "LOCATION_CODE"] = "" + context.vaccine_df.loc[3, "LOCATION_CODE_TYPE_URI"] = "" + context.vaccine_df.loc[4, ["UNIQUE_ID", "PERSON_SURNAME"]] = ["", "no_unique_identifiers"] + context.vaccine_df.loc[5, ["UNIQUE_ID_URI", "PERSON_SURNAME"]] = ["", "no_unique_identifiers"] + context.vaccine_df.loc[6, "PRIMARY_SOURCE"] = "" + context.vaccine_df.loc[7, "VACCINATION_PROCEDURE_CODE"] = "" + context.vaccine_df.loc[8, "SITE_CODE"] = " " + context.vaccine_df.loc[9, "SITE_CODE_TYPE_URI"] = " " + context.vaccine_df.loc[10, "LOCATION_CODE"] = " " + context.vaccine_df.loc[11, "LOCATION_CODE_TYPE_URI"] = " " + context.vaccine_df.loc[12, ["UNIQUE_ID", "PERSON_SURNAME"]] = [" ", "no_unique_id"] + context.vaccine_df.loc[13, ["UNIQUE_ID_URI", "PERSON_SURNAME"]] = [" ", "no_unique_id_uri"] + context.vaccine_df.loc[14, "PRIMARY_SOURCE"] = " " + context.vaccine_df.loc[15, "VACCINATION_PROCEDURE_CODE"] = " " + context.vaccine_df.loc[16, "PRIMARY_SOURCE"] = "test" + context.vaccine_df.loc[17, "ACTION_FLAG"] = "" + context.vaccine_df.loc[18, "ACTION_FLAG"] = " " + create_batch_file(context) + + +@given("batch file is created for below data where mandatory field for site, location and unique uri values are invalid") +@ignore_if_local_run +def valid_batch_file_is_created_with_invalid_mandatory_fields(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "UNIQUE_ID_URI"] = "invalid_uri" + context.vaccine_df.loc[1, "SITE_CODE_TYPE_URI"] = "invalid_uri" + context.vaccine_df.loc[2, "LOCATION_CODE_TYPE_URI"] = "invalid_uri" + create_batch_file(context) + + +@given("batch file is created for below data where action flag has different cases") +@ignore_if_local_run +def valid_batch_file_is_created_with_different_action_flag_cases(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "ACTION_FLAG"] = "NEW" + context.vaccine_df.loc[1, "ACTION_FLAG"] = "New" + context.vaccine_df.loc[2, "ACTION_FLAG"] = "new" + context.vaccine_df.loc[3, "ACTION_FLAG"] = "nEw" + create_batch_file(context) + + +@given("batch file is created for below data where non mandatory fields are empty string") +@ignore_if_local_run +def valid_batch_file_is_created_with_empty_non_mandatory_fields(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "NHS_NUMBER"] = " " + context.vaccine_df.loc[1, "VACCINATION_PROCEDURE_TERM"] = " " + context.vaccine_df.loc[2, "VACCINE_PRODUCT_CODE"] = " " + context.vaccine_df.loc[3, "VACCINE_PRODUCT_TERM"] = " " + context.vaccine_df.loc[4, "VACCINE_MANUFACTURER"] = " " + context.vaccine_df.loc[5, "BATCH_NUMBER"] = " " + context.vaccine_df.loc[6, "SITE_OF_VACCINATION_CODE"] = " " + context.vaccine_df.loc[7, "SITE_OF_VACCINATION_TERM"] = " " + context.vaccine_df.loc[8, "ROUTE_OF_VACCINATION_CODE"] = " " + context.vaccine_df.loc[9, "ROUTE_OF_VACCINATION_TERM"] = " " + context.vaccine_df.loc[10, "DOSE_SEQUENCE"] = " " + context.vaccine_df.loc[11, "DOSE_UNIT_CODE"] = " " + context.vaccine_df.loc[12, "DOSE_UNIT_TERM"] = " " + context.vaccine_df.loc[13, "INDICATION_CODE"] = " " + create_batch_file(context) + + +@given("batch file is created for below data where non mandatory fields are missing") +@ignore_if_local_run +def valid_batch_file_is_created_with_missing_non_mandatory_fields(datatable, context): + build_dataFrame_using_datatable(datatable, context) + context.vaccine_df.loc[0, "NHS_NUMBER"] = "" + context.vaccine_df.loc[1, "VACCINATION_PROCEDURE_TERM"] = "" + context.vaccine_df.loc[2, "VACCINE_PRODUCT_CODE"] = "" + context.vaccine_df.loc[3, "VACCINE_PRODUCT_TERM"] = "" + context.vaccine_df.loc[4, "VACCINE_MANUFACTURER"] = "" + context.vaccine_df.loc[5, "BATCH_NUMBER"] = "" + context.vaccine_df.loc[6, "SITE_OF_VACCINATION_CODE"] = "" + context.vaccine_df.loc[7, "SITE_OF_VACCINATION_TERM"] = "" + context.vaccine_df.loc[8, "ROUTE_OF_VACCINATION_CODE"] = "" + context.vaccine_df.loc[9, "ROUTE_OF_VACCINATION_TERM"] = "" + context.vaccine_df.loc[10, "DOSE_SEQUENCE"] = "" + context.vaccine_df.loc[11, "DOSE_UNIT_CODE"] = "" + context.vaccine_df.loc[12, "DOSE_UNIT_TERM"] = "" + context.vaccine_df.loc[13, "INDICATION_CODE"] = "" + create_batch_file(context) diff --git a/tests/e2e_automation/features/batchTests/Steps/test_delete_batch_steps.py b/tests/e2e_automation/features/batchTests/Steps/test_delete_batch_steps.py new file mode 100644 index 0000000000..67d2e178d1 --- /dev/null +++ b/tests/e2e_automation/features/batchTests/Steps/test_delete_batch_steps.py @@ -0,0 +1,100 @@ +import pandas as pd +from pytest_bdd import given, scenarios, then, when +from src.objectModels.batch.batch_file_builder import build_batch_file + +from features.APITests.steps.common_steps import validate_imms_event_table_by_operation, validVaccinationRecordIsCreated +from features.APITests.steps.test_create_steps import validate_imms_delta_table_by_ImmsID +from features.APITests.steps.test_delete_steps import validate_imms_delta_table_by_deleted_ImmsID + +from .batch_common_steps import build_dataFrame_using_datatable, create_batch_file, ignore_if_local_run + +scenarios("batchTests/delete_batch.feature") + + +@given("batch file is created for below data as full dataset and each record has a valid delete record in the same file") +@ignore_if_local_run +def valid_batch_file_is_created_with_details(datatable, context): + build_dataFrame_using_datatable(datatable, context) + df_new = context.vaccine_df.copy() + df_update = df_new.copy() + df_update["ACTION_FLAG"] = "DELETE" + context.vaccine_df = pd.concat([df_new, df_update], ignore_index=True) + create_batch_file(context) + + +@given("I have created a valid vaccination record through API") +def create_valid_vaccination_record_through_api(context): + validVaccinationRecordIsCreated(context) + print(f"Created Immunization record with ImmsID: {context.ImmsID}") + + +@when("An delete to above vaccination record is made through batch file upload") +def upload_batch_file_to_s3_for_update(context): + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + context.vaccine_df.loc[ + 0, + [ + "NHS_NUMBER", + "PERSON_FORENAME", + "PERSON_SURNAME", + "PERSON_GENDER_CODE", + "PERSON_DOB", + "PERSON_POSTCODE", + "ACTION_FLAG", + "UNIQUE_ID", + "UNIQUE_ID_URI", + ], + ] = [ + context.create_object.contained[1].identifier[0].value, + context.create_object.contained[1].name[0].given[0], + context.create_object.contained[1].name[0].family, + context.create_object.contained[1].gender, + context.create_object.contained[1].birthDate.replace("-", ""), + context.create_object.contained[1].address[0].postalCode, + "DELETE", + context.create_object.identifier[0].value, + context.create_object.identifier[0].system, + ] + create_batch_file(context) + context.vaccine_df.loc[0, "IMMS_ID"] = context.ImmsID + + +@then("The delta and imms event table will be populated with the correct data for api created event") +@given("The delta and imms event table will be populated with the correct data for api created event") +def validate_imms_delta_table_for_api_created_event(context): + validate_imms_event_table_by_operation(context, "created") + validate_imms_delta_table_by_ImmsID(context) + + +@when("Delete above vaccination record is made through batch file upload with mandatory field missing") +def upload_batch_file_to_s3_for_update_with_mandatory_field_missing(context): + # Build base record + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + + base_fields = { + "NHS_NUMBER": context.create_object.contained[1].identifier[0].value, + "PERSON_FORENAME": context.create_object.contained[1].name[0].given[0], + "PERSON_SURNAME": context.create_object.contained[1].name[0].family, + "PERSON_GENDER_CODE": context.create_object.contained[1].gender, + "PERSON_DOB": "", + "PERSON_POSTCODE": context.create_object.contained[1].address[0].postalCode, + "ACTION_FLAG": "DELETE", + "UNIQUE_ID": context.create_object.identifier[0].value, + "UNIQUE_ID_URI": context.create_object.identifier[0].system, + } + context.vaccine_df.loc[0, list(base_fields.keys())] = list(base_fields.values()) + + create_batch_file(context) + context.vaccine_df.loc[0, "IMMS_ID"] = context.ImmsID + + +@then("The imms event table status will be updated to delete and no change to record detail") +def validate_imms_event_table_for_delete_event(context): + validate_imms_event_table_by_operation(context, "deleted") + + +@then("The delta table will have delete entry with no change to record detail") +def validate_delta_table_for_delete_event(context): + validate_imms_delta_table_by_deleted_ImmsID(context) diff --git a/tests/e2e_automation/features/batchTests/Steps/test_update_batch_steps.py b/tests/e2e_automation/features/batchTests/Steps/test_update_batch_steps.py new file mode 100644 index 0000000000..166884355e --- /dev/null +++ b/tests/e2e_automation/features/batchTests/Steps/test_update_batch_steps.py @@ -0,0 +1,232 @@ +import pandas as pd +from pytest_bdd import given, scenarios, then, when +from src.objectModels.batch.batch_file_builder import build_batch_file +from utilities.batch_file_helper import read_and_validate_bus_ack_file_content +from utilities.enums import GenderCode +from utilities.error_constants import ERROR_MAP + +from features.APITests.steps.common_steps import ( + The_request_will_have_status_code, + send_update_for_immunization_event, + valid_json_payload_is_created, + validate_etag_in_header, + validate_imms_event_table_by_operation, + validVaccinationRecordIsCreated, +) +from features.APITests.steps.test_create_steps import validate_imms_delta_table_by_ImmsID +from features.APITests.steps.test_update_steps import validate_delta_table_for_updated_event + +from .batch_common_steps import build_dataFrame_using_datatable, create_batch_file + +scenarios("batchTests/update_batch.feature") + + +@given("batch file is created for below data as full dataset and each record has a valid update record in the same file") +def valid_batch_file_is_created_with_details(datatable, context): + build_dataFrame_using_datatable(datatable, context) + df_new = context.vaccine_df.copy() + df_update = df_new.copy() + df_update[["ACTION_FLAG", "EXPIRY_DATE"]] = ["UPDATE", "20281231"] + context.vaccine_df = pd.concat([df_new, df_update], ignore_index=True) + context.expected_version = 2 + create_batch_file(context) + + +@given("I have created a valid vaccination record through API") +def create_valid_vaccination_record_through_api(context): + validVaccinationRecordIsCreated(context) + print(f"Created Immunization record with ImmsID: {context.ImmsID}") + + +@when("An update to above vaccination record is made through batch file upload") +def upload_batch_file_to_s3_for_update(context): + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + context.vaccine_df.loc[ + 0, + [ + "NHS_NUMBER", + "PERSON_FORENAME", + "PERSON_SURNAME", + "PERSON_GENDER_CODE", + "PERSON_DOB", + "PERSON_POSTCODE", + "ACTION_FLAG", + "UNIQUE_ID", + "UNIQUE_ID_URI", + ], + ] = [ + context.create_object.contained[1].identifier[0].value, + context.create_object.contained[1].name[0].given[0], + context.create_object.contained[1].name[0].family, + context.create_object.contained[1].gender, + context.create_object.contained[1].birthDate.replace("-", ""), + context.create_object.contained[1].address[0].postalCode, + "UPDATE", + context.create_object.identifier[0].value, + context.create_object.identifier[0].system, + ] + context.expected_version = 2 + create_batch_file(context) + + +@then("The delta and imms event table will be populated with the correct data for api created event") +@given("The delta and imms event table will be populated with the correct data for api created event") +def validate_imms_delta_table_for_api_created_event(context): + validate_imms_event_table_by_operation(context, "created") + validate_imms_delta_table_by_ImmsID(context) + + +@when("Send a update for Immunization event created with vaccination detail being updated through API request") +def send_update_for_immunization_event_with_vaccination_detail_updated(context): + valid_json_payload_is_created(context) + row = context.vaccine_df.loc[0] + context.immunization_object.contained[1].identifier[0].value = row["NHS_NUMBER"] + context.immunization_object.contained[1].name[0].given[0] = row["PERSON_FORENAME"] + context.immunization_object.contained[1].name[0].family = row["PERSON_SURNAME"] + reverse_gender_map = {v.value: v.name for v in GenderCode} + code = row["PERSON_GENDER_CODE"] + context.immunization_object.contained[1].gender = reverse_gender_map.get(code, "unknown") + context.immunization_object.contained[ + 1 + ].birthDate = f"{row['PERSON_DOB'][:4]}-{row['PERSON_DOB'][4:6]}-{row['PERSON_DOB'][6:]}" + context.immunization_object.contained[1].address[0].postalCode = row["PERSON_POSTCODE"] + context.immunization_object.identifier[0].value = row["UNIQUE_ID"] + context.immunization_object.identifier[0].system = row["UNIQUE_ID_URI"] + send_update_for_immunization_event(context) + + +@then("Api request will be successful and tables will be updated correctly") +def api_request_will_be_successful_and_tables_will_be_updated_correctly(context): + The_request_will_have_status_code(context, 200) + validate_etag_in_header(context) + validate_imms_event_table_by_operation(context, "updated") + validate_delta_table_for_updated_event(context) + + +@when("Update to above vaccination record is made through batch file upload with mandatory field missing") +def upload_batch_file_to_s3_for_update_with_mandatory_field_missing(context): + # Build base record + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + + base_fields = { + "NHS_NUMBER": context.create_object.contained[1].identifier[0].value, + "PERSON_FORENAME": context.create_object.contained[1].name[0].given[0], + "PERSON_SURNAME": context.create_object.contained[1].name[0].family, + "PERSON_GENDER_CODE": context.create_object.contained[1].gender, + "PERSON_DOB": context.create_object.contained[1].birthDate.replace("-", ""), + "PERSON_POSTCODE": context.create_object.contained[1].address[0].postalCode, + "ACTION_FLAG": "UPDATE", + "UNIQUE_ID": context.create_object.identifier[0].value, + "UNIQUE_ID_URI": context.create_object.identifier[0].system, + } + + context.vaccine_df.loc[0, list(base_fields.keys())] = list(base_fields.values()) + + context.vaccine_df = pd.concat([context.vaccine_df.loc[[0]]] * 19, ignore_index=True) + + missing_cases = { + 0: {"SITE_CODE": "", "PERSON_SURNAME": "empty_site_code"}, + 1: {"SITE_CODE_TYPE_URI": "", "PERSON_SURNAME": "empty_site_code_uri"}, + 2: {"LOCATION_CODE": "", "PERSON_SURNAME": "empty_location_code"}, + 3: {"LOCATION_CODE_TYPE_URI": "", "PERSON_SURNAME": "empty_location_code_uri"}, + 4: {"UNIQUE_ID": "", "PERSON_SURNAME": "no_unique_identifiers"}, + 5: {"UNIQUE_ID_URI": "", "PERSON_SURNAME": "no_unique_identifiers"}, + 6: {"PRIMARY_SOURCE": "", "PERSON_SURNAME": "empty_primary_source"}, + 7: {"VACCINATION_PROCEDURE_CODE": "", "PERSON_SURNAME": "no_procedure_code"}, + 8: {"SITE_CODE": " ", "PERSON_SURNAME": "no_site_code"}, + 9: {"SITE_CODE_TYPE_URI": " ", "PERSON_SURNAME": "no_site_code_uri"}, + 10: {"LOCATION_CODE": " ", "PERSON_SURNAME": "no_location_code"}, + 11: {"LOCATION_CODE_TYPE_URI": " ", "PERSON_SURNAME": "no_location_code_uri"}, + 12: {"UNIQUE_ID": " ", "PERSON_SURNAME": "no_unique_id"}, + 13: {"UNIQUE_ID_URI": " ", "PERSON_SURNAME": "no_unique_id_uri"}, + 14: {"PRIMARY_SOURCE": " ", "PERSON_SURNAME": "no_primary_source"}, + 15: {"VACCINATION_PROCEDURE_CODE": " ", "PERSON_SURNAME": "empty_procedure_code"}, + 16: {"PRIMARY_SOURCE": "test", "PERSON_SURNAME": "no_primary_source"}, + 17: {"ACTION_FLAG": "", "PERSON_SURNAME": "invalid_action_flag"}, + 18: {"ACTION_FLAG": " ", "PERSON_SURNAME": "invalid_action_flag"}, + } + + # Apply all missing-field modifications + for row_idx, updates in missing_cases.items(): + for col, value in updates.items(): + context.vaccine_df.loc[row_idx, col] = value + + create_batch_file(context) + + +@then("bus ack will have error records for all the updated records in the batch file") +def all_records_are_processed_successfully_in_the_batch_file(context): + file_rows = read_and_validate_bus_ack_file_content(context, False, True) + all_valid = validate_bus_ack_file_for_error_by_surname(context, file_rows) + assert all_valid, "One or more records failed validation checks" + + +def validate_bus_ack_file_for_error_by_surname(context, file_rows) -> bool: + if not file_rows: + print("No rows found in BUS ACK file for failed records") + return False + + overall_valid = True + + for batch_idx, row in context.vaccine_df.iterrows(): + bus_ack_row_number = batch_idx + 2 + + row_data_list = file_rows.get(bus_ack_row_number) + + if not row_data_list: + print(f"Batch row {batch_idx}: No BUS ACK entry found for row number {bus_ack_row_number}") + overall_valid = False + continue + + surname = str(row.get("PERSON_SURNAME", "")).strip() + expected_error = surname + expected_diagnostic = ERROR_MAP.get(expected_error, {}).get("diagnostics") + + for row_data in row_data_list: + i = row_data["row"] + fields = row_data["fields"] + row_valid = True + + header_response_code = fields[1] + issue_severity = fields[2] + issue_code = fields[3] + response_code = fields[6] + response_display = fields[7] + imms_id = fields[11] + operation_outcome = fields[12] + message_delivery = fields[13] + + if header_response_code != "Fatal Error": + print(f"Row {i}: HEADER_RESPONSE_CODE is not 'Fatal Error'") + row_valid = False + if issue_severity != "Fatal": + print(f"Row {i}: ISSUE_SEVERITY is not 'Fatal'") + row_valid = False + if issue_code != "Fatal Error": + print(f"Row {i}: ISSUE_CODE is not 'Fatal Error'") + row_valid = False + if response_code != "30002": + print(f"Row {i}: RESPONSE_CODE is not '30002'") + row_valid = False + if response_display != "Business Level Response Value - Processing Error": + print(f"Row {i}: RESPONSE_DISPLAY is not expected value") + row_valid = False + if imms_id: + print(f"Row {i}: IMMS_ID should be null but is populated") + row_valid = False + if message_delivery != "False": + print(f"Row {i}: MESSAGE_DELIVERY is not 'False'") + row_valid = False + + if operation_outcome != expected_diagnostic: + print( + f"Row {i}: operation_outcome '{operation_outcome}' does not match " + f"expected diagnostics '{expected_diagnostic}' for surname '{expected_error}'" + ) + row_valid = False + + overall_valid = overall_valid and row_valid + + return overall_valid diff --git a/tests/e2e_automation/features/batchTests/__init__.py b/tests/e2e_automation/features/batchTests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_automation/features/batchTests/batch_file_validation.feature b/tests/e2e_automation/features/batchTests/batch_file_validation.feature new file mode 100644 index 0000000000..ccd80ecf8a --- /dev/null +++ b/tests/e2e_automation/features/batchTests/batch_file_validation.feature @@ -0,0 +1,102 @@ +@Batch_File_Validation_Feature +Feature: Validate the file level and columns validations for vaccination batch file + +@vaccine_type_COVID @supplier_name_MAVIS +Scenario Outline: verify that vaccination file will be rejected if file name format is invalid + Given batch file is created for below data with filename and extension + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has failure status for processed batch file + And bus ack file will not be created + And Audit table will have '', '' and '' for the processed batch file + + Examples: + | invalidFilename | file_extension | status | queue_name | error_details | + | HP_Vaccinations_v5_YGM41 | csv | Failed | unknown_unknown | Initial file validation failed: invalid file key | + | HPV_Vaccinations_v5_YGM41 | pdf | Failed | unknown_unknown | Initial file validation failed: invalid file key | + | HPV_Vaccination_v5_YGM41 | csv | Failed | unknown_unknown | Initial file validation failed: invalid file key | + | HPV_Vaccinations_v0_YGM41 | csv | Failed | unknown_unknown | Initial file validation failed: invalid file key | + | HPV_Vaccinations_v0_ABC12 | csv | Failed | unknown_unknown | Initial file validation failed: invalid file key | + +@vaccine_type_HPV @supplier_name_MAVIS +Scenario: verify that vaccination file will be rejected if the processed file is duplicate + Given batch file is created for below data as full dataset + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + When same batch file is uploaded again in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created for duplicate batch file upload + And inf ack file has failure status for processed batch file + And bus ack file will not be created + And Audit table will have 'Not processed - Duplicate', 'MAVIS_HPV' and 'None' for the processed batch file + +@vaccine_type_FLU @supplier_name_MAVIS +Scenario: verify that vaccination file will be rejected if file is empty + Given Empty batch file is created + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will not be created + And Audit table will have 'Not processed - Empty file', 'MAVIS_FLU' and 'None' for the processed batch file + +@vaccine_type_MENACWY @supplier_name_TPP +Scenario: verify that vaccination file will be rejected if columns are missing + Given batch file is created with missing columns for below data + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has failure status for processed batch file + And bus ack file will not be created + And Audit table will have 'Failed', 'TPP_MENACWY' and 'File headers are invalid.' for the processed batch file + +@vaccine_type_COVID @supplier_name_EMIS +Scenario: verify that vaccination file will be rejected if column order is invalid + Given batch file is created with invalid column order for below data + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has failure status for processed batch file + And bus ack file will not be created + And Audit table will have 'Failed', 'EMIS_COVID' and 'File headers are invalid.' for the processed batch file + +@vaccine_type_FLU @supplier_name_SONAR +Scenario: verify that vaccination file will be rejected if file delimiter is invalid + Given batch file is created with invalid delimiter for below data + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has failure status for processed batch file + And bus ack file will not be created + And Audit table will have 'Failed', 'SONAR_FLU' and 'File headers are invalid.' for the processed batch file + +@vaccine_type_3IN1 @supplier_name_TPP +Scenario: verify that vaccination file will be rejected if one of the column name is invalid + Given batch file is created with invalid column name for patient surname for below data + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has failure status for processed batch file + And bus ack file will not be created + And Audit table will have 'Failed', 'TPP_3IN1' and 'File headers are invalid.' for the processed batch file + +@vaccine_type_3IN1 @supplier_name_EMIS +Scenario: verify that vaccination file will be rejected if additional column is present + Given batch file is created with additional column person age for below data + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has failure status for processed batch file + And bus ack file will not be created + And Audit table will have 'Failed', 'EMIS_3IN1' and 'File headers are invalid.' for the processed batch file \ No newline at end of file diff --git a/tests/e2e_automation/features/batchTests/create_batch.feature b/tests/e2e_automation/features/batchTests/create_batch.feature new file mode 100644 index 0000000000..e3c7313794 --- /dev/null +++ b/tests/e2e_automation/features/batchTests/create_batch.feature @@ -0,0 +1,277 @@ +@Create_Batch_Feature @functional +Feature: Create the immunization event for a patient through batch file + +@smoke +@delete_cleanup_batch @vaccine_type_HPV @supplier_name_TPP +Scenario: Verify that full dataset vaccination record will be created through batch file + Given batch file is created for below data as full dataset + | patient_id | unique_id | + | Random | Valid_NhsNumber | + | InvalidInPDS | InvalidInPDS_NhsNumber| + | SFlag | SFlag_NhsNumber | + | Mod11_NHS | Mod11_NhSNumber | + | OldNHSNo | OldNHSNo | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + +@smoke +@delete_cleanup_batch @vaccine_type_MMR @supplier_name_TPP +Scenario: Verify that minimum dataset vaccination record will be created through batch file + Given batch file is created for below data as minimum dataset + | patient_id | unique_id | + | Random | Valid_NhsNumber | + | InvalidInPDS | InvalidInPDS_NhsNumber| + | SFlag | SFlag_NhsNumber | + | Mod11_NHS | Mod11_NhSNumber | + | OldNHSNo | OldNHSNo | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + +@vaccine_type_FLU @supplier_name_MAVIS +Scenario: Verify that vaccination record will be get rejected if date_and_time is invalid in batch file + Given batch file is created for below data where date_and_time field has invalid date + | patient_id | unique_id | + | Random | Fail-future_occurrence-invalid_OccurrenceDateTime | + | Random | Fail-invalid_batch_occurrence-invalid_OccurrenceDateTime | + | Random | Fail-nonexistent-invalid_OccurrenceDateTime | + | Random | Fail-empty-empty_OccurrenceDateTime | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And all records are rejected in the bus ack file and no imms id is generated + And Audit table will have correct status, queue name and record count for the processed batch file + +@vaccine_type_6IN1 @supplier_name_EMIS +Scenario: verify that vaccination record will be get rejected if recorded_date is invalid in batch file + Given batch file is created for below data where recorded field has invalid date + | patient_id | unique_id | + | Random | Fail-future_date-invalid_recorded | + | Random | Fail-invalid_format-invalid_recorded | + | Random | Fail-nonexistent-invalid_recorded | + | Random | Fail-empty-empty_recorded | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And all records are rejected in the bus ack file and no imms id is generated + And Audit table will have correct status, queue name and record count for the processed batch file + +@vaccine_type_4IN1 @supplier_name_TPP +Scenario: verify that vaccination record will be get rejected if expiry_date is invalid in batch file + Given batch file is created for below data where expiry field has invalid date + | patient_id | unique_id | + | Random | Fail-invalid_format-invalid_expirationDate | + | Random | Fail-nonexistent-invalid_expirationDate | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And all records are rejected in the bus ack file and no imms id is generated + And Audit table will have correct status, queue name and record count for the processed batch file + +@vaccine_type_FLU @supplier_name_MAVIS +Scenario: verify that vaccination record will be get rejected if Person date of birth is invalid in batch file + Given batch file is created for below data where Person date of birth field has invalid date + | patient_id | unique_id | + | Random | Fail-future_date-future_DateOfBirth | + | Random | Fail-invalid_format-invalid_DateOfBirth | + | Random | Fail-nonexistent-invalid_DateOfBirth | + | Random | Fail-empty-missing_DateOfBirth | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And all records are rejected in the bus ack file and no imms id is generated + And Audit table will have correct status, queue name and record count for the processed batch file + +@vaccine_type_FLU @supplier_name_MAVIS +Scenario: verify that vaccination record will be get rejected if Person nhs number, name and gender is invalid in batch file + Given batch file is created for below data where Person detail has invalid data + | patient_id | unique_id | + | Random | Fail-invalid_NhsNumber-invalid_nhsnumber_length | + | Random | Fail-not_MOD11_NhsNumber-invalid_mod11_nhsnumber | + | Random | Fail-empty_patient_forename-no_forename | + | Random | Fail-empty_patient_name-empty_forename_surname | + | Random | Fail-empty_patient_surname-no_surname | + | Random | Fail-invalid_gender_code-invalid_gender | + | Random | Fail-invalid_gender-invalid_gender | + | Random | Fail-empty_gender-missing_gender | + | Random | Fail-white_space_forename-empty_array_item_forename | + | Random | Fail-white_space_surname-empty_surname | + | Random | Fail-name_length_36-max_len_surname | + | Random | Fail-name_length_181-max_len_forename | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And all records are rejected in the bus ack file and no imms id is generated + And Audit table will have correct status, queue name and record count for the processed batch file + +@vaccine_type_BCG @supplier_name_TPP +Scenario: verify that vaccination record will be get successful if performer is invalid in batch file + Given batch file is created for below data where performer detail has invalid data + | patient_id | unique_id | + | Random | empty_performer_forename | + | Random | empty_performer_Surname | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + +@vaccine_type_ROTAVIRUS @supplier_name_TPP +Scenario: verify that vaccination record will be get successful with different valid value in gender field + Given batch file is created for below data where person detail has valid values + | patient_id | unique_id | + | Random | gender_value_0 | + | Random | gender_value_1 | + | Random | gender_value_2 | + | Random | gender_value_9 | + | Random | gender_value_Not-Known | + | Random | gender_value_male | + | Random | gender_value_female | + | Random | gender_value_not-Specified | + | Random | patient_surname_max_length | + | Random | patient_forename_max_length | + | Random | patient_forename_max_length_multiple_values | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + +@vaccine_type_FLU @supplier_name_MAVIS +Scenario: verify that vaccination record will be get rejected if mandatory fields for site, location and unique identifiers are missing in batch file + Given batch file is created for below data where mandatory fields for site, location, action flag, primary source and unique identifiers are missing + | patient_id | unique_id | + | Random | Fail-empty_site_code-empty_site_code | + | Random | Fail-empty_site_Code_uri-empty_site_code_uri | + | Random | Fail-empty_location_code-empty_location_code | + | Random | Fail-empty_location_code_uri-empty_location_code_uri | + | Random | Fail-empty_unique_id-no_unique_identifiers | + | Random | Fail-empty_unique_id_uri-no_unique_identifiers | + | Random | Fail-empty_primary_source-empty_primary_source | + | Random | Fail-empty_procedure_code-no_procedure_code | + | Random | Fail-white_space_site_code-no_site_code | + | Random | Fail-white_space_site_Code_uri-no_site_code_uri | + | Random | Fail-white_space_location_code-no_location_code | + | Random | Fail-white_space_location_Code_uri-no_location_code_uri| + | Random | Fail-white_space_unique_id-no_unique_id | + | Random | Fail-white_space_unique_id_uri-no_unique_id_uri | + | Random | Fail-white_space_primary_source-no_primary_source | + | Random | Fail-white_space_procedure_code-empty_procedure_code | + | Random | Fail-invalid_primary_source-no_primary_source | + | Random | Fail-empty_action_flag-invalid_action_flag | + | Random | Fail-white_space_action_flag-invalid_action_flag | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And all records are rejected in the bus ack file and no imms id is generated + And Audit table will have correct status, queue name and record count for the processed batch file + +@delete_cleanup_batch @vaccine_type_HIB @supplier_name_EMIS +Scenario: verify that vaccination record will be successful if mandatory field for site, location and unique URI are invalid in batch file + Given batch file is created for below data where mandatory field for site, location and unique uri values are invalid + | patient_id | unique_id | + | Random | invalid_unique_id_uri- | + | Random | invalid_site_Code_uri- | + | Random | invalid_location_Code_uri | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + + +@delete_cleanup_batch @vaccine_type_MENACWY @supplier_name_TPP +Scenario: verify that vaccination record will be get successful if action flag has different cases + Given batch file is created for below data where action flag has different cases + | patient_id | unique_id | + | Random | Action_flag_NEW | + | Random | Action_flag_New | + | Random | Action_flag_new | + | Random | Action_flag_nEw | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + + +@vaccine_type_3IN1 @supplier_name_TPP +Scenario: verify that vaccination record will be get rejected if non mandatory fields are empty string in batch file + Given batch file is created for below data where non mandatory fields are empty string + | patient_id | unique_id | + | Random | Fail-empty_NHS_Number-empty_NHSNumber | + | Random | Fail-empty_procedure_term-empty_procedure_term | + | Random | Fail-empty_product_code-empty_product_code | + | Random | Fail-empty_product_term-empty_product_term | + | Random | Fail-empty_VACCINE_MANUFACTURER-empty_manufacturer | + | Random | Fail-empty_batch_number-empty_lot_number | + | Random | Fail-empty_site_OF_vaccination-empty_vaccine_site_code | + | Random | Fail-empty_site_OF_vaccination_term-empty_vaccine_site_term | + | Random | Fail-empty_ROUTE_OF_vaccination-empty_route_code | + | Random | Fail-empty_ROUTE_OF_vaccination_term-empty_route_term | + | Random | Fail-empty_DOSE_SEQUENCE-doseNumberPositiveInt_PositiveInteger | + | Random | Fail-empty_dose_unit_code-empty_doseQuantity_code | + | Random | Fail-empty_dose_unit_term-empty_doseQuantity_term | + | Random | Fail-empty_indication_code-empty_indication_code | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And all records are rejected in the bus ack file and no imms id is generated + And Audit table will have correct status, queue name and record count for the processed batch file + +@delete_cleanup_batch @vaccine_type_3IN1 @supplier_name_TPP +Scenario: verify that vaccination record will be get successful if non mandatory fields are missing in batch file + Given batch file is created for below data where non mandatory fields are missing + | patient_id | unique_id | + | Random | no_NHS_Number | + | Random | no_procedure_term | + | Random | no_product_code | + | Random | no_product_term | + | Random | no_VACCINE_MANUFACTURER | + | Random | no_batch_number | + | Random | no_site_OF_vaccination | + | Random | no_site_OF_vaccination_term | + | Random | no_ROUTE_OF_vaccination | + | Random | no_ROUTE_OF_vaccination_term | + | Random | no_DOSE_SEQUENCE | + | Random | no_dose_unit_code | + | Random | no_dose_unit_term | + | Random | no_indication_code | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file diff --git a/tests/e2e_automation/features/batchTests/delete_batch.feature b/tests/e2e_automation/features/batchTests/delete_batch.feature new file mode 100644 index 0000000000..53b19a8b44 --- /dev/null +++ b/tests/e2e_automation/features/batchTests/delete_batch.feature @@ -0,0 +1,48 @@ +@Delete_Batch_Feature @functional +Feature: Create the immunization event for a patient through batch file and update the record from batch or Api calls + +@smoke +@vaccine_type_BCG @supplier_name_TPP +Scenario: Delete immunization event for a patient through batch file + Given batch file is created for below data as full dataset and each record has a valid delete record in the same file + | patient_id | unique_id | + | Random | Valid_NhsNumber | + | InvalidInPDS | InvalidInPDS_NhsNumber| + | SFlag | SFlag_NhsNumber | + | Mod11_NHS | Mod11_NhSNumber | + | OldNHSNo | OldNHSNo | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'deleted' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + And The delta table will be populated with the correct data for all deleted records in batch file + +@vaccine_type_MENB @patient_id_Random @supplier_name_EMIS +Scenario: Verify that the API vaccination record will be successful deleted by batch file upload + Given I have created a valid vaccination record through API + And The delta and imms event table will be populated with the correct data for api created event + When An delete to above vaccination record is made through batch file upload + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table status will be updated to delete and no change to record detail + And The delta table will have delete entry with no change to record detail + +@vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the API vaccination record will be successful deleted and batch file will successful with mandatory field missing + Given I have created a valid vaccination record through API + When Delete above vaccination record is made through batch file upload with mandatory field missing + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And The imms event table status will be updated to delete and no change to record detail + And The delta table will have delete entry with no change to record detail diff --git a/tests/e2e_automation/features/batchTests/update_batch.feature b/tests/e2e_automation/features/batchTests/update_batch.feature new file mode 100644 index 0000000000..4b819ae3b5 --- /dev/null +++ b/tests/e2e_automation/features/batchTests/update_batch.feature @@ -0,0 +1,63 @@ +@Update_Batch_Feature @functional +Feature: Create the immunization event for a patient through batch file and update the record from batch or Api calls + +@smoke +@delete_cleanup_batch @vaccine_type_MMR @supplier_name_TPP +Scenario: Update immunization event for a patient through batch file + Given batch file is created for below data as full dataset and each record has a valid update record in the same file + | patient_id | unique_id | + | Random | Valid_NhsNumber | + | InvalidInPDS | InvalidInPDS_NhsNumber| + | SFlag | SFlag_NhsNumber | + | Mod11_NHS | Mod11_NhSNumber | + | OldNHSNo | OldNHSNo | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'updated' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + And The delta table will be populated with the correct data for all updated records in batch file + +@Delete_cleanUp @vaccine_type_ROTAVIRUS @patient_id_Random @supplier_name_EMIS +Scenario: Verify that the API vaccination record will be successful updated by batch file upload + Given I have created a valid vaccination record through API + And The delta and imms event table will be populated with the correct data for api created event + When An update to above vaccination record is made through batch file upload + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'updated' event for records in batch file + And The delta table will be populated with the correct data for all updated records in batch file + +@Delete_cleanUp @vaccine_type_6IN1 @patient_id_Random @supplier_name_TPP +Scenario: Verify that the batch vaccination record will be successful updated by API request + Given batch file is created for below data as full dataset + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + When Send a update for Immunization event created with vaccination detail being updated through API request + Then Api request will be successful and tables will be updated correctly + +@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the API vaccination record will be successful updated and batch file will fail upload due to mandatory field missing + Given I have created a valid vaccination record through API + When Update to above vaccination record is made through batch file upload with mandatory field missing + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will have error records for all the updated records in the batch file + And The delta and imms event table will be populated with the correct data for api created event diff --git a/tests/e2e_automation/features/conftest.py b/tests/e2e_automation/features/conftest.py new file mode 100644 index 0000000000..e63160e702 --- /dev/null +++ b/tests/e2e_automation/features/conftest.py @@ -0,0 +1,148 @@ +import os +from pathlib import Path + +import allure +import pytest +from dotenv import load_dotenv +from utilities.api_fhir_immunization_helper import empty_folder +from utilities.api_gen_token import get_tokens +from utilities.api_get_header import get_delete_url_header +from utilities.apigee.apigee_env_helpers import use_temp_apigee_apps +from utilities.apigee.ApigeeApp import ApigeeApp +from utilities.apigee.ApigeeOnDemandAppManager import ApigeeOnDemandAppManager +from utilities.aws_token import refresh_sso_token, set_aws_session_token +from utilities.context import ScenarioContext +from utilities.enums import SupplierNameWithODSCode +from utilities.http_requests_session import http_requests_session + +# Ignore F403 * imports. Pytest BDD requires common steps to be imported in conftest +from features.APITests.steps.common_steps import * # noqa: F403 +from features.batchTests.Steps.batch_common_steps import * # noqa: F403 + + +@pytest.hookimpl(tryfirst=True) +def pytest_bdd_after_step(request, feature, scenario, step, step_func, step_func_args): + if not step.failed: + message = f"✅ Step Passed: **{step.name}" + allure.attach(message, name=f"Step Passed: {step.name}", attachment_type=allure.attachment_type.TEXT) + + +@pytest.hookimpl(tryfirst=True) +def pytest_bdd_step_error(request, feature, scenario, step, exception): + message = f"❌ Step failed! **{step.name}** \n Error: {exception}" + allure.attach(message, name=f"Step Failed: {step.name}", attachment_type=allure.attachment_type.TEXT) + + +@pytest.hookimpl(tryfirst=True) +def pytest_bdd_before_scenario(request, feature, scenario): + allure.dynamic.epic("Immunization Service") + allure.dynamic.suite(feature.name) # Separates features into distinct suites + allure.dynamic.feature(feature.name) # Ensures correct feature grouping + allure.dynamic.title((scenario.name).replace("_", " ")) + + +@pytest.fixture(scope="session", autouse=True) +def setup_environment(): + empty_folder("output/allure-results") + empty_folder("output/allure-report") + load_dotenv() + + +@pytest.fixture(scope="session") +def global_context(): + aws_profile_name = os.getenv("aws_profile_name") + refresh_sso_token(aws_profile_name) if os.getenv( + "aws_token_refresh", "false" + ).strip().lower() == "true" else set_aws_session_token() + + +@pytest.fixture(scope="session") +def temp_apigee_apps(): + if use_temp_apigee_apps(): + apigee_app_mgr = ApigeeOnDemandAppManager() + created_apps = apigee_app_mgr.setup_apps_and_product() + + for test_app in created_apps: + os.environ[f"{test_app.supplier}_client_Id"] = test_app.client_id + os.environ[f"{test_app.supplier}_client_Secret"] = test_app.client_secret + + yield created_apps + + apigee_app_mgr.teardown_apps_and_product() + else: + yield None + + +@pytest.fixture +def context(request, global_context, temp_apigee_apps: list[ApigeeApp] | None) -> ScenarioContext: + ctx = ScenarioContext() + ctx.aws_profile_name = os.getenv("aws_profile_name") + + node = request.node + tags = [marker.name for marker in node.own_markers] + + env_vars = [ + "auth_url", + "token_url", + "callback_url", + "baseUrl", + "username", + "scope", + "S3_env", + "LOCAL_RUN_WITHOUT_S3_UPLOAD", + ] + for var in env_vars: + setattr(ctx, var, os.getenv(var)) + + project_root = Path(__file__).resolve().parents[1] + # Define working_directory at root level + working_dir = project_root / "batch_files_directory" + + working_dir.mkdir(exist_ok=True) + ctx.working_directory = str(working_dir) + + for tag in tags: + if tag.startswith("vaccine_type_"): + ctx.vaccine_type = tag.split("vaccine_type_")[1] + if tag.startswith("patient_id_"): + ctx.patient_id = tag.split("patient_id_")[1] + if tag.startswith("supplier_name_"): + ctx.supplier_name = tag.split("supplier_name_")[1] + get_tokens(ctx, ctx.supplier_name) + ctx.supplier_name = tag.split("supplier_name_")[1] + ctx.supplier_ods_code = SupplierNameWithODSCode[ctx.supplier_name].value + + return ctx + + +def pytest_bdd_after_scenario(request, feature, scenario): + tags = set(getattr(scenario, "tags", [])) | set(getattr(feature, "tags", [])) + context = request.getfixturevalue("context") + get_delete_url_header(context) + + if "Delete_cleanUp" in tags: + if context.ImmsID is not None: + print(f"\n Delete Request is {context.url}/{context.ImmsID}") + context.response = http_requests_session.delete(f"{context.url}/{context.ImmsID}", headers=context.headers) + assert context.response.status_code == 204, ( + f"Expected status code 204, but got {context.response.status_code}. Response: {context.response.json()}" + ) + else: + print("Skipping delete: ImmsID is None") + + if "delete_cleanup_batch" in tags: + get_tokens(context, context.supplier_name) + context.vaccine_df["IMMS_ID_CLEAN"] = ( + context.vaccine_df["IMMS_ID"].astype(str).str.replace("Immunization#", "", regex=False) + ) + + for imms_id in context.vaccine_df["IMMS_ID_CLEAN"].dropna().unique(): + delete_url = f"{context.url}/{imms_id}" + print(f"Sending DELETE request to: {delete_url}") + response = http_requests_session.delete(delete_url, headers=context.headers) + + assert response.status_code == 204, ( + f" Failed to delete {imms_id}: expected 204, got {response.status_code}. Response: {response.text}" + ) + + print("✅ All IMMS_IDs deleted successfully.") diff --git a/tests/e2e_automation/input/testData.csv b/tests/e2e_automation/input/testData.csv new file mode 100644 index 0000000000..203450e4bd --- /dev/null +++ b/tests/e2e_automation/input/testData.csv @@ -0,0 +1,1009 @@ +id,nhs_number,family_name,given_name,gender,birth_date,address_text,address_line,city,district,state,postal_code,country,start_date,end_date +ValidNHS,9449309981,test1,test2,unknown,1980-11-01,Validate Obf,"1, obf_2",obf_3,obf_4,obf_5,LS01 1AB,obf_7,2000-01-01,2025-01-01 +NUllNHS,,test1,test2,unknown,1980-11-01,Validate Obf,"1, obf_2",obf_3,obf_4,obf_5,LS01 1AB,obf_7,2000-01-01,2025-01-01 +SFlag,9449310475,test1,test2,unknown,1980-11-01,Validate Obf,"1, obf_2",obf_3,obf_4,obf_5,LS01 1AB,obf_7,2000-01-01,2025-01-01 +InvalidInPDS,9449310599,test1,test2,unknown,1980-11-01,Validate Obf,"1, obf_2",obf_3,obf_4,obf_5,LS01 1AB,obf_7,2000-01-01,2025-01-01 +InvalidMOD11Check,1234567890,test1,test2,unknown,1980-11-01,Validate Obf,"1, obf_2",obf_3,obf_4,obf_5,LS01 1AB,obf_7,2000-01-01,2025-01-01 +OldNHSNo,9452372230,test1,test2,unknown,1980-11-01,Validate Obf,"1, obf_2",obf_3,obf_4,obf_5,LS01 1AB,obf_7,2000-01-01,2025-01-01 +SupersedeNhsNo,9467351307,test1,test2,unknown,1980-11-01,Validate Obf,"1, obf_2",obf_3,obf_4,obf_5,LS01 1AB,obf_7,2000-01-01,2025-01-01 +Invalid_NHS,9461267665,STERLING,SOKHI,unknown,2007-11-25,217A,ROUNDHAY ROAD,City,district,State,LS8 4HS,UK,2013-12-22,2033-01-01 +Mod11_NHS,2788584652,STERLING,Sal,unknown,2007-11-25,217A,ROUNDHAY ROAD,City,district,State,LS8 4HS,UK,2013-12-22,2033-01-01 +Mod11_NHS,3510670485,STERLING,Sall,unknown,2007-11-25,217A,ROUNDHAY ROAD,City,district,State,LS8 4HS,UK,2013-12-22,2033-01-01 +Mod11_NHS,5309741852,STERLING,Sally,unknown,2007-11-25,217A,ROUNDHAY ROAD,City,district,State,LS8 4HS,UK,2013-12-22,2033-01-01 +Valid_NHS,9001066569,CAMELIA,DURGAN,male,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001076599,ROBIN,VAUGHAN,male,2000-08-11,10 Jamaica Road Torquay,MO,City,district,State,NW4 2PJ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001089518,ALEX,PAUCEK,female,2009-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001106188,TAYLOR,INGHAM,female,2009-09-09,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001060471,KAY,QUINN,male,2001-08-11,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001063152,OAKLEY,DRAPER,male,2000-08-11,44 Rathbone Street,Ventnor,City,district,State,E14 8AH,UK,2025-06-04,2033-01-01 +Valid_NHS,9001063411,AIDE,BEATTY,female,2008-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001070353,URI,STAFFORD,female,2009-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001070426,EDWARD,DANIEL,female,2008-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001073441,CLAUDE,PHILLIPS,male,2009-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001074138,YENCH,SPEEDBIRD,male,2000-01-01,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,M16 0RA,UK,2025-06-04,2033-01-01 +Valid_NHS,9001082637,BRYANT,ERNSER,male,2008-12-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001100562,RICARDO,RUECKER,female,2008-06-04,01 Moore,BISHOP,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001065899,LYMAN,HYATT,female,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001068618,IONA,GRIFFITHS,male,2009-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001071635,MILAN,HILLS,male,2000-10-13,Address Line 2 Address Line 1 Address Line 3,ADDRESS LINE 4,City,district,State,RG1 3BW,UK,2025-06-04,2033-01-01 +Valid_NHS,9001073751,SOLEDAD,HERMISTON,female,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001071414,KAY,YORKE,male,2008-09-10,8 High Holborn Stockport,STOCKPORT,City,district,State,SR8 1HQ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001077501,LORIA,KAUTZER,male,2005-12-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001087434,LAURIE,KANE,female,2001-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 9JZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001107990,MIRTA,HERMAN,male,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9735567210,CARLIE,LUNDON,female,2009-01-24,10 WHARFE VIEW,WETHERBY,City,district,State,LS22 6HB,UK,2016-05-26,2033-01-01 +Valid_NHS,9735571668,AUDREY,REA,female,2007-05-05,26 THE MOUNT,ALWOODLEY,City,district,State,LS17 7QU,UK,2021-12-03,2033-01-01 +Valid_NHS,9001060374,MARLOWE,GOODMAN,male,2009-06-04,Campus City New Campus 41 Berkeley Road Westbury Park,MANCHESTER,City,district,State,M16 0RA,UK,2025-06-04,2033-01-01 +Valid_NHS,9001063985,PAIGE,WELLER,male,2009-11-24,32 Fenchurch Street Thatcham,THATCHAM,City,district,State,NE22 5AX,UK,2025-06-04,2033-01-01 +Valid_NHS,9001064914,TERESIA,KESSLER,male,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001073867,LOUVENIA,HOWE,female,2008-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001077811,IDA,DRAPER,male,2008-08-22,Portland 42 Palace Gardens Terrace Portland,GERMANY,City,district,State,ZZ99 4FZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9735570130,JULES,MUDIE,male,2001-08-16,11 NEWTON GROVE,LEEDS,City,district,State,LS7 4HW,UK,2014-09-11,2033-01-01 +Valid_NHS,9001064469,NANCEY,LEDNER,male,2000-05-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001069177,KAY,GOODMAN,female,2009-06-04,Campus City New Campus 41 Berkeley Road Westbury Park,MANCHESTER,City,district,State,M16 0RA,UK,2025-06-04,2033-01-01 +Valid_NHS,9001074928,JOESPH,WAELCHI,male,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001075428,LAQUANDA,STROSIN,female,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001077234,GABI,DAVENPORT,female,2001-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001079431,MATTHEW,FAZ,female,2003-12-14,Sydenham House,Mill Ct,City,district,State,TN24 8DN,UK,2025-06-04,2033-01-01 +Valid_NHS,9001085172,HARPER,ADAMS,female,2001-08-11,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001088538,WHITNEY,NEWTON,female,2000-07-24,Leeds 39 Maida Avenue Al,LEEDS,City,district,State,B17 9DQ,UK,2025-06-04,2033-01-01 +Valid_NHS,9735567296,TREVOR,DEBES,male,2007-03-31,ELMWOOD,CHURCH LANE,City,district,State,LS22 5AU,UK,2016-05-24,2033-01-01 +Valid_NHS,9735570181,SARA,SLEE,female,2007-06-01,10 KING EDWARD STREET,SCUNTHORPE,City,district,State,DN16 1LH,UK,2011-05-20,2033-01-01 +Valid_NHS,9001075347,MORGAN,SCHOEN,male,2008-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001063306,EVAN,JASKOLSKI,male,2000-03-18,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001064302,MINDA,ALTENWERTH,female,2008-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001065430,KELSEY,ADAMS,male,2008-10-19,01,Moore,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001066127,RODRIGO,DOUGLAS,female,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001073557,KAY,YORKE,male,2009-06-04,Campus City New Campus 41 Berkeley Road Westbury Park,MANCHESTER,City,district,State,M16 0RA,UK,2025-06-04,2033-01-01 +Valid_NHS,9001076939,MOSES,ROWE,female,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001078095,VIVIEN,FELIX,male,2001-08-11,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001078419,SIDNEY,YOUNG,male,2008-09-01,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,ZZ99 6CZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9735569272,WINNIE,VERDI,female,2001-06-22,16 THE VIEW,ALWOODLEY,City,district,State,LS17 7NB,UK,2020-11-25,2033-01-01 +Valid_NHS,9735571153,RODNEY,NOYES,male,2009-05-19,THORP ARCH MILL,MILL LANE,City,district,State,LS23 7DZ,UK,2013-09-01,2033-01-01 +Valid_NHS,9001081967,ROSINA,GUSIKOWSKI,male,2009-01-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001060277,ROLAND,FEENEY,female,2008-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001062024,IONA,OLIVIER,male,2001-06-04,9 Islington High Street Manchester,MT,City,district,State,BS2 8HE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001062121,MICAELA,KLOCKO,male,2008-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9001066461,GINO,LEMKE,male,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001070868,LAVERNE,KRAJCIK,male,2007-06-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-06-04,2033-01-01 +Valid_NHS,9001096093,RUSS,GULGOWSKI,male,2003-05-01,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-06-04,2033-01-01 +Valid_NHS,9694559553,CLIVE,KEHOE,male,2006-07-17,223 STATION ROAD,NEW WALTHAM,City,district,State,DN36 4PF,UK,2017-10-09,2033-01-01 +Valid_NHS,9460160255,SU,BAFFOE,female,2006-07-17,FLAT 1,72 LEEDS ROAD,City,district,State,HG2 8BG,UK,2013-12-22,2033-01-01 +Valid_NHS,9482914031,DEBRA,DAVID,female,2006-07-17,10 TIDESWELL COURT,SCUNTHORPE,City,district,State,DN15 7UH,UK,2011-06-06,2033-01-01 +Valid_NHS,9657970563,TAMMY,HANLON,female,2006-07-17,12 ST. JOSEPH DRIVE,HULL,City,district,State,HU4 6TF,UK,2013-05-18,2033-01-01 +Valid_NHS,9692108279,ABBY,HOSE,unknown,2006-07-17,102 EDWARD STREET,OLDHAM,City,district,State,OL9 7SX,UK,2007-10-28,2033-01-01 +Valid_NHS,9484893546,CARLIE,HUXLEY,female,2006-07-17,153 NORTH ROAD,CARNFORTH,City,district,State,LA5 9NG,UK,2008-10-07,2033-01-01 +Valid_NHS,9650353135,QIAOLIAN,OU,female,2006-07-17,1 THIRNBY COURT,KIRKBY LONSDALE,City,district,State,LA6 2BZ,UK,2010-02-06,2033-01-01 +Valid_NHS,9694432553,FRANK,WILLER,male,2006-07-17,2 CHANDLERS CLOSE,NEW WALTHAM,City,district,State,DN36 4WH,UK,2007-12-26,2033-01-01 +Valid_NHS,9726303613,RATANJOT,DAIRE,female,2006-07-17,678 CROMWELL ROAD,GRIMSBY,City,district,State,DN37 9LJ,UK,2011-12-30,2033-01-01 +Valid_NHS,9468055566,PRABHROOP,XXXXX-NON-PDS-ONE,male,2006-07-17,1 FENBY CLOSE,BRADFORD,City,district,State,BD4 8QZ,UK,2011-04-10,2033-01-01 +Valid_NHS,9692955257,DEBRA,ISLE,female,2006-07-17,2 CRAWFORD LANE,KESGRAVE,City,district,State,IP5 2GY,UK,2017-03-14,2033-01-01 +Valid_NHS,9727063268,FIONA,HOWELL,female,2006-07-17,41 FRONT STREET,CHIRTON,City,district,State,NE29 7QN,UK,2011-07-24,2033-01-01 +Valid_NHS,9490339628,FAYE,RIGBY,female,2006-07-17,1 GANDY STREET,KENDAL,City,district,State,LA9 7AE,UK,2014-09-03,2033-01-01 +Valid_NHS,9686375708,LISA,WARING,female,2006-07-17,BRISBANE HOUSE,NELSON ROAD,City,district,State,PO1 4NQ,UK,2007-04-22,2033-01-01 +Valid_NHS,9461216947,BASYA,MEYER,female,2006-07-17,1 NEW HEY MOOR HOUSES,SHEPLEY,City,district,State,HD8 8ES,UK,2013-12-22,2033-01-01 +Valid_NHS,9470076362,CARIS,FELTWELL,female,2006-07-17,LOW BRATHAY,CLAPPERSGATE,City,district,State,LA22 0HN,UK,2016-02-08,2033-01-01 +Valid_NHS,9661132836,ALICIA,TUCKER,unknown,2006-07-17,305 ASHBY HIGH STREET,SCUNTHORPE,City,district,State,DN16 2RY,UK,2010-07-20,2033-01-01 +Valid_NHS,5992766251,LANGLEY,IZZARD,male,2006-07-17,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4QZ,UK,2023-02-01,2033-01-01 +Valid_NHS,5992824340,SIDNEY,HALFORD,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-02-10,2033-01-01 +Valid_NHS,9728486642,NATHAN,HAND,male,2006-07-17,56 HENDERSON AVENUE,SCUNTHORPE,City,district,State,DN15 7RU,UK,2009-08-14,2033-01-01 +Valid_NHS,5993542020,ALEX,GRIFFITHS,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-06-16,2033-01-01 +Valid_NHS,5993750111,BRETT,OLIVIER,female,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-07-19,2033-01-01 +Valid_NHS,5993754370,VIVIEN,GOODMAN,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-07-19,2033-01-01 +Valid_NHS,5993748303,HAYDEN,MCGLYNN,female,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-07-19,2033-01-01 +Valid_NHS,5993742674,JAMA,STROMAN,female,2006-07-17,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 7YZ,UK,2023-07-19,2033-01-01 +Valid_NHS,9730846901,SIMON,FENNAR,male,2006-07-17,1 CONINGSBY ROAD,SCUNTHORPE,City,district,State,DN17 2HH,UK,2013-05-11,2033-01-01 +Valid_NHS,9730840768,GERDA,GILDEA,female,2006-07-17,157 SCOTTER ROAD,SCUNTHORPE,City,district,State,DN15 8AU,UK,2012-01-30,2033-01-01 +Valid_NHS,9731531319,FANCY,RYAN,female,2006-07-17,78 HIGH STREET,BROUGHTON,City,district,State,DN20 0HY,UK,2014-04-13,2033-01-01 +Valid_NHS,5997491587,NIKKI,WILLIAMS,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,5997483428,GEORGETTA,ROBERTS,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,5997487466,SHAWN,YORKE,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,5997484882,TAYLOR,JACKSON,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,5997485293,HILDA,HEGMANN,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,5997488764,ANDREW,JOHNSTON,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,5997492346,ROMEO,SCHUMM,male,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,5997486478,DEMARCUS,LEBSACK,female,2006-07-17,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-17,2033-01-01 +Valid_NHS,9734905120,WENDY,HOY,female,2007-11-25,42 BURKE STREET,SCUNTHORPE,City,district,State,DN15 6DP,UK,2024-01-01,2033-01-01 +Valid_NHS,9733749036,HOLLY,BROOKS,female,2007-11-25,11 JACKSON STREET,GRIMSBY,City,district,State,DN31 1TS,UK,2013-05-08,2033-01-01 +Valid_NHS,5998207378,YVONNE,GOODMAN,female,2007-11-25,01,Moore,City,district,State,N8 7RE,UK,2024-09-16,2033-01-01 +Valid_NHS,9733086207,HILDA,HINTON,female,2007-11-25,CLIFF HILL HOUSE,HAXEY ROAD,City,district,State,DN9 1DE,UK,2020-08-10,2033-01-01 +Valid_NHS,9732545178,JULIE,BARTON,female,2007-11-25,10 TREECE GARDENS,BARTON-UPON-HUMBER,City,district,State,DN18 5UL,UK,2022-09-14,2033-01-01 +Valid_NHS,5997081796,ADRIAN,UPTON,female,2007-11-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-06-11,2033-01-01 +Valid_NHS,5997081435,DEANE,YEATES,female,2007-11-25,01,Moore,City,district,State,N8 7RE,UK,2024-06-11,2033-01-01 +Valid_NHS,5997032140,TAYLOR,KANE,male,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-06-05,2033-01-01 +Valid_NHS,5996921855,ISIDRO,ABERNATHY,male,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-05-28,2033-01-01 +Valid_NHS,5995984241,FRANKIE,WELLER,male,2007-11-25,01,Moore,City,district,State,N8 7RE,UK,2024-02-02,2033-01-01 +Valid_NHS,9730598827,ADRIAN,DAULBY,male,2007-11-25,1 WINGFIELD STREET,BRADFORD,City,district,State,BD3 0AH,UK,2011-05-29,2033-01-01 +Valid_NHS,5995303112,ZOE,YORKE,male,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-11-17,2033-01-01 +Valid_NHS,9730380929,GRETA,TURNER,female,2007-11-25,2 OLD DAIRY,BARROW-UPON-HUMBER,City,district,State,DN19 7ST,UK,2011-10-10,2033-01-01 +Valid_NHS,9729712387,RUPERT,WARLOW,male,2007-11-25,1 KIRKDALE GARDENS,LONG EATON,City,district,State,NG10 3JA,UK,2009-10-26,2033-01-01 +Valid_NHS,5993865202,SIDNEY,NIELSEN,male,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-07-31,2033-01-01 +Valid_NHS,9728951000,MELVIN,CHEYNE,male,2007-11-25,23 EAST PARK ROAD,HALIFAX,City,district,State,HX3 5DX,UK,2019-08-17,2033-01-01 +Valid_NHS,5993585986,OAKLEY,VENABLES,male,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-07-03,2033-01-01 +Valid_NHS,5993095385,DALE,VARDY,male,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-03-23,2033-01-01 +Valid_NHS,5993019816,WHITNEY,MANSELL,male,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-03-16,2033-01-01 +Valid_NHS,9464935863,CANDYCE,KIMBLE,female,2007-11-25,131 MARY STREET,SCUNTHORPE,City,district,State,DN15 7PX,UK,2015-02-25,2033-01-01 +Valid_NHS,5990073895,EVELYN,ELGAR,female,2007-11-25,7 Littleton Close,Kenilworth,City,district,State,CV8 2WA,UK,2007-11-25,2033-01-01 +Valid_NHS,9650869700,SIAN,BARRAT,unknown,2007-11-25,1 BUTTERMERE CRESCENT,HUMBERSTON,City,district,State,DN36 4AD,UK,2016-04-09,2033-01-01 +Valid_NHS,9460799299,INDIGO,TWIVEY,unknown,2007-11-25,SCHOOL HOUSE,DRUMMOND ROAD,City,district,State,BD8 8DA,UK,2013-12-22,2033-01-01 +Valid_NHS,9461411049,JOYCE,JAMESON,male,2007-11-25,1 EDWARDIAN DRIVE,BRIDLINGTON,City,district,State,YO15 3TF,UK,2013-12-22,2033-01-01 +Valid_NHS,9725650565,WADHA,TOVIAND,female,2007-11-25,2 CHELSEA WALK,CLEETHORPES,City,district,State,DN35 0QP,UK,2014-06-30,2033-01-01 +Valid_NHS,9461373864,LAIS,VENKTESH,male,2007-11-25,165 THORNABY ROAD,THORNABY,City,district,State,TS17 6LG,UK,2013-12-22,2033-01-01 +Valid_NHS,9692689808,YAASMEENA,ADENIYI,female,2007-11-25,101 BERKELEY STREET,SCUNTHORPE,City,district,State,DN15 6BL,UK,2015-04-08,2033-01-01 +Valid_NHS,9725562291,ROBIN,HUXLEY,male,2007-11-25,128 HAYCROFT STREET,GRIMSBY,City,district,State,DN31 2ED,UK,2021-08-17,2033-01-01 +Valid_NHS,9485554761,DUDLEY,GAD,male,2007-11-25,10 BRENTLEA CRESCENT,HEYSHAM,City,district,State,LA3 2BT,UK,2015-01-28,2033-01-01 +Valid_NHS,9658033954,CHAN JUAN,PAK,unknown,2007-11-25,12 ROXBY ROAD,WINTERTON,City,district,State,DN15 9SX,UK,2013-03-28,2033-01-01 +Valid_NHS,9692598853,SOPHIE,DAWBER,female,2007-11-25,1 THE POPLARS,EPWORTH,City,district,State,DN9 1FF,UK,2010-08-29,2033-01-01 +Valid_NHS,9459948847,OSMOND,LAND,unknown,2007-11-25,BEWICK COURT,PRINCESS SQUARE,City,district,State,NE1 8EG,UK,2013-12-22,2033-01-01 +Valid_NHS,9473771177,PERDITA,HATHWAY,female,2007-11-25,11 FIRST AVENUE,BRIDLINGTON,City,district,State,YO15 2JW,UK,2016-02-08,2033-01-01 +Valid_NHS,9486587191,HUGH,EYES,male,2007-11-25,1 ASH GROVE,BRIGG,City,district,State,DN20 8AQ,UK,2012-11-08,2033-01-01 +Valid_NHS,9691804417,CLIVE,PETRIE,male,2007-11-25,17 VICARAGE LANE,SCAWBY,City,district,State,DN20 9AJ,UK,2013-11-03,2033-01-01 +Valid_NHS,9692956067,MIRIAM,HYLAND,female,2007-11-25,ALBION COTTAGE,CHURCH ROAD,City,district,State,IP17 1SX,UK,2012-03-27,2033-01-01 +Valid_NHS,9727265243,ELLICE,CROFT,unknown,2007-11-25,10 LINCOLN DRIVE,BARTON-UPON-HUMBER,City,district,State,DN18 6DJ,UK,2017-02-08,2033-01-01 +Valid_NHS,9726469511,JEANNE,KIBBEY,female,2007-11-25,1 WELLINGTON CLOSE,SOUTH KILLINGHOLME,City,district,State,DN40 3HN,UK,2010-02-18,2033-01-01 +Valid_NHS,9657957788,KITTY,CARMEL,unknown,2007-11-25,10 WEETSLADE CRESCENT,DUDLEY,City,district,State,NE23 7LJ,UK,2015-12-01,2033-01-01 +Valid_NHS,9725796985,FRANK,GOUGH,male,2007-11-25,2 ST. MICHAELS ROAD,GRIMSBY,City,district,State,DN34 5SH,UK,2011-11-13,2033-01-01 +Valid_NHS,9473724721,NATALEE,COLECLOUGH,female,2007-11-25,1 SUMMERFIELD TERRACE,LOW WESTWOOD,City,district,State,NE17 7PR,UK,2016-02-08,2033-01-01 +Valid_NHS,9476656220,WILLIS,OLDHAM,male,2007-11-25,311 LACEBY ROAD,GRIMSBY,City,district,State,DN34 5LP,UK,2011-12-29,2033-01-01 +Valid_NHS,9486376638,LILLY,HOWDEN,unknown,2007-11-25,10 GOLGOTHA VILLAGE,LANCASTER,City,district,State,LA1 3DZ,UK,2012-04-19,2033-01-01 +Valid_NHS,9457921976,JADEN,FYLES,male,2007-11-25,1 THE LINDALES,BARNSLEY,City,district,State,S75 2DT,UK,2013-12-22,2033-01-01 +Valid_NHS,9470078063,ROYALE,GEHLERT,male,2007-11-25,10 WOBORROW ROAD,HEYSHAM,City,district,State,LA3 2PN,UK,2016-02-08,2033-01-01 +Valid_NHS,9725672097,SELENA,FERRIE,unknown,2007-11-25,19 CHEAPSIDE,WALTHAM,City,district,State,DN37 0HF,UK,2014-10-17,2033-01-01 +Valid_NHS,9726200466,DORA,PALMAS,female,2007-11-25,1 HARRISON STREET,GRIMSBY,City,district,State,DN31 2DX,UK,2019-08-25,2033-01-01 +Valid_NHS,9651101393,TAMSYN,LOWERY,female,2007-11-25,179 BEECH LANE,LEEDS,City,district,State,LS9 6SF,UK,2011-10-05,2033-01-01 +Valid_NHS,9465713155,MERLYN,BLUETT,male,2007-11-25,1 ISLE CLOSE,CROWLE,City,district,State,DN17 4NR,UK,2015-07-21,2033-01-01 +Valid_NHS,9651687444,MORRIS,FRASER,male,2007-11-25,510 ANLABY ROAD,HULL,City,district,State,HU3 6SZ,UK,2010-03-06,2033-01-01 +Valid_NHS,9727056121,JEANNE,LOUDON,female,2007-11-25,39 LABURNUM AVENUE,WALLSEND,City,district,State,NE28 8HG,UK,2010-01-21,2033-01-01 +Valid_NHS,9457925955,FREDDIE,GAYNARD,male,2007-11-25,2 ST. BEDES PARK,SUNDERLAND,City,district,State,SR2 7DZ,UK,2013-12-22,2033-01-01 +Valid_NHS,9650115498,TAAMIR,OSEI-MANU,male,2007-11-25,1 GLEAVES ROAD,ECCLES,City,district,State,M30 0FU,UK,2011-12-08,2033-01-01 +Valid_NHS,9475752005,EDMUND,WOMACK,male,2007-11-25,ARCON HOUSE,BRIDGE ROAD,City,district,State,LA1 4TJ,UK,2015-09-23,2033-01-01 +Valid_NHS,9472764967,BOBBI,HORSFIELD,female,2007-11-25,10 KENTMERE GROVE,MORECAMBE,City,district,State,LA4 5UF,UK,2016-02-08,2033-01-01 +Valid_NHS,9658012310,ROBERT,ARCHER,male,2007-11-25,GRANARY COTTAGE,HIGH STREET,City,district,State,DN21 4LU,UK,2016-10-16,2033-01-01 +Valid_NHS,9726507715,NIGEL,HALLAM,male,2007-11-25,1 BOULEVARD WALK,GRIMSBY,City,district,State,DN31 2JT,UK,2021-01-03,2033-01-01 +Valid_NHS,9490289116,MAGGIE,LOGAN,female,2007-11-25,2 WOODSIDE CLOSE,ENDMOOR,City,district,State,LA8 0HP,UK,2009-10-20,2033-01-01 +Valid_NHS,9691418290,JORDAN,CONLEY,male,2007-11-25,25 SOUTHGATE,SCUNTHORPE,City,district,State,DN15 6SU,UK,2019-12-06,2033-01-01 +Valid_NHS,9462204616,PALMER,AVERIES,male,2007-11-25,101 ALFRETON ROAD,SOUTH NORMANTON,City,district,State,DE55 2BJ,UK,2007-07-19,2033-01-01 +Valid_NHS,9473722184,RACQUEL,TALL,female,2007-11-25,119 WHITEHALL ROAD EAST,BIRKENSHAW,City,district,State,BD11 2LH,UK,2016-02-08,2033-01-01 +Valid_NHS,9674972919,RUTH,BEEVER,female,2007-11-25,1 ASHBROOK ROAD,DAGENHAM,City,district,State,RM10 7ED,UK,2017-04-08,2033-01-01 +Valid_NHS,9725745981,QING YUAN,DAI,female,2007-11-25,HATCLIFFE HOUSE,WASHDYKE LANE,City,district,State,DN40 2HX,UK,2020-08-09,2033-01-01 +Valid_NHS,9726219493,MERVYN,FRANK,male,2007-11-25,10 HUMBERSTONE ROAD,GRIMSBY,City,district,State,DN32 8BP,UK,2016-10-26,2033-01-01 +Valid_NHS,9726301548,ALICE,PETRIE,female,2007-11-25,1 MULLWAY,IMMINGHAM,City,district,State,DN40 1RF,UK,2020-11-07,2033-01-01 +Valid_NHS,9458016029,SAMARA,ROWE,female,2007-11-25,4 LINDEN DRIVE,DARLINGTON,City,district,State,DL3 8PW,UK,2013-12-22,2033-01-01 +Valid_NHS,9725687965,MINA,TRIPP,female,2007-11-25,1 BOWERS AVENUE,GRIMSBY,City,district,State,DN31 2BG,UK,2017-07-22,2033-01-01 +Valid_NHS,9692726657,OLIVE,PARKS,unknown,2007-11-25,1 GEORGE STREET,BROUGHTON,City,district,State,DN20 0LB,UK,2009-12-04,2033-01-01 +Valid_NHS,9693683641,BETTY,MEALEY,female,2007-11-25,SPRINGWOOD COTTAGE,BROUGHTON ROAD,City,district,State,DN15 0DD,UK,2017-08-20,2033-01-01 +Valid_NHS,9726813492,GUIREN,YE,male,2007-11-25,63 BOSTOCK LODGE,BATH ROAD,City,district,State,RG7 5JA,UK,2011-11-22,2033-01-01 +Valid_NHS,9650430687,LUCY,MACKEY,female,2007-11-25,WELBY CRAGG CARAVAN,ELLEL,City,district,State,LA2 0QB,UK,2008-01-20,2033-01-01 +Valid_NHS,9650179364,AMANDA,YARROW,female,2007-11-25,100 WESTGATE,MORECAMBE,City,district,State,LA3 3DG,UK,2009-09-20,2033-01-01 +Valid_NHS,9727373070,CASSIE,SAWELL,unknown,2007-11-25,10 FOWLER ROAD,SCUNTHORPE,City,district,State,DN16 1PH,UK,2022-08-20,2033-01-01 +Valid_NHS,5992466398,DARCEL,CONN,male,2007-11-25,Bad Oyenhausen,notgiven,City,district,State,ZZ99 4QZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992464441,NUMBERS,WILKINSON,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992467564,NOELLE,PRICE,female,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-25,2033-01-01 +Valid_NHS,5992463305,TERINA,HEATHCOTE,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992473629,AMADA,ROBEL,male,2007-11-25,Bad Oyenhausen,notgiven,City,district,State,ZZ99 4QZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992474307,NOEL,WOLFF,male,2007-11-25,Bad Oyenhausen,notgiven,City,district,State,ZZ99 4QZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992463399,AUBREY,DIBBERT,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992471707,WILLIE,EFFERTZ,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992464158,ASSUNTA,HETTINGER,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992462554,JERAMY,GULGOWSKI,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992458751,LARHONDA,BERGNAUM,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992462635,ADAM,CASPER,male,2007-11-25,Bad Oyenhausen,notgiven,City,district,State,ZZ99 4QZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992465510,MARGOT,PFEFFER,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992473831,MACK,SCHULTZ,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992461043,ALDEN,HICKLE,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992473939,SHERWOOD,KUHLMAN,female,2007-11-25,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-11-25,2033-01-01 +Valid_NHS,5992662561,IONA,VAUGHAN,female,2007-11-25,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4QZ,UK,2023-01-16,2033-01-01 +Valid_NHS,9727959563,MAY,BRABIN,female,2007-11-25,2 THE COOMBE,STREATLEY,City,district,State,RG8 9QY,UK,2017-07-13,2033-01-01 +Valid_NHS,5992953531,JUNIOR,FRAMI,male,2007-11-25,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4QZ,UK,2023-03-03,2033-01-01 +Valid_NHS,5992974040,NIKKI,AYERS,female,2007-11-25,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4QZ,UK,2023-03-07,2033-01-01 +Valid_NHS,5993017597,ZARA,QUINN,female,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-03-14,2033-01-01 +Valid_NHS,9728381387,ANDREA,CARTER,female,2007-11-25,FLAT A,IVY FLATS 14 IVY TERRACE,City,district,State,CF37 2SB,UK,2012-08-08,2033-01-01 +Valid_NHS,5993284315,YASMINE,ELSON,female,2007-11-25,01,Moore,City,district,State,N8 7RE,UK,2023-04-27,2033-01-01 +Valid_NHS,9728508212,LINDA,KITSON,female,2007-11-25,1 BECK WALK,WINTERTON,City,district,State,DN15 9XH,UK,2020-03-06,2033-01-01 +Valid_NHS,5993901284,HAN,WATSICA,male,2007-11-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-08-03,2033-01-01 +Valid_NHS,5994492272,HARPER,OWEN,female,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-09-07,2033-01-01 +Valid_NHS,5994869694,IONA,WALKER,female,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-10-13,2033-01-01 +Valid_NHS,9730434123,BART,DOLBY,male,2007-11-25,1 RIDGE VIEW,BRIGG,City,district,State,DN20 8UF,UK,2019-08-21,2033-01-01 +Valid_NHS,9730501602,NORMAN,TIMSON,male,2007-11-25,THE CONIFERS,CHURCH LANE,City,district,State,DN39 6TB,UK,2017-10-02,2033-01-01 +Valid_NHS,5995843494,CAROL,YORKE,female,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-01-22,2033-01-01 +Valid_NHS,9730828776,CONNIE,PEDDER,female,2007-11-25,HUNGATE HOUSE,HUNGATE,City,district,State,DN18 5PN,UK,2018-04-20,2033-01-01 +Valid_NHS,5996203510,KETURAH,KLOCKO,female,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-02-21,2033-01-01 +Valid_NHS,5996522209,JEANINE,WEHNER,female,2007-11-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-03-20,2033-01-01 +Valid_NHS,5996539772,AARON,WISOKY,male,2007-11-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-03-21,2033-01-01 +Valid_NHS,9732062525,EVELYN,MCGRAW,unknown,2007-11-25,11 CORNWALL CLOSE,KIRTON LINDSEY,City,district,State,DN21 4PR,UK,2017-02-16,2033-01-01 +Valid_NHS,9732781327,NATHAN,ASKEW,male,2007-11-25,1 BRAYBROOKE ROAD,LIVERPOOL,City,district,State,L11 3DY,UK,2015-07-10,2033-01-01 +Valid_NHS,9732841257,JUDY,DOULBY,female,2007-11-25,11 QUEEN STREET,BARTON-UPON-HUMBER,City,district,State,DN18 5QP,UK,2020-10-13,2033-01-01 +Valid_NHS,5998236726,GARTH,BEIER,female,2007-11-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-19,2033-01-01 +Valid_NHS,9733177318,ELIAS,COUPE,male,2007-11-25,32 VICARAGE GARDENS,SCUNTHORPE,City,district,State,DN15 7BB,UK,2019-01-06,2033-01-01 +Valid_NHS,5998352971,URI,FOSTER,male,2007-11-25,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,ZZ99 6CZ,UK,2024-10-02,2033-01-01 +Valid_NHS,5998777344,WESTLEY,GRIFFITHS,female,2007-11-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-11-07,2033-01-01 +Valid_NHS,9734211099,TOBY,KEAY,male,2007-11-25,11 RYDER GARDENS,LEEDS,City,district,State,LS8 1JS,UK,2019-02-27,2033-01-01 +Valid_NHS,9734502832,SHARON,SIMMS,female,2007-11-25,1 FIR TREE GROVE,LEEDS,City,district,State,LS17 7ES,UK,2015-10-29,2033-01-01 +Valid_NHS,9734905147,TRUDI,JAYCOCK,female,2007-12-30,3 NORTHLANDS FARM COTTAGE,NORTHLANDS ROAD,City,district,State,DN15 9UP,UK,2022-04-13,2033-01-01 +Valid_NHS,5999008794,TOMMY,ROOB,male,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-12-30,2033-01-01 +Valid_NHS,5998996844,IONA,DAVENPORT,female,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-12-25,2033-01-01 +Valid_NHS,5998999711,HARLEY,GOODMAN,female,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-12-25,2033-01-01 +Valid_NHS,9733417149,JULIA,COBB,unknown,2007-12-30,2 HARRISON CLOSE,WINTERINGHAM,City,district,State,DN15 9PJ,UK,2020-03-30,2033-01-01 +Valid_NHS,5998330943,CORNELIUS,MORISSETTE,male,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2024-09-30,2033-01-01 +Valid_NHS,5997855619,MADISON,NIKOLAUS,male,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-08-14,2033-01-01 +Valid_NHS,5997518264,ULI,FELIX,male,2007-12-30,01,Moore,City,district,State,N8 7RE,UK,2024-07-18,2033-01-01 +Valid_NHS,9732633107,JACKIE,WROE,female,2007-12-30,17 WEST END,WINTERINGHAM,City,district,State,DN15 9NR,UK,2012-10-30,2033-01-01 +Valid_NHS,9732299932,ANNA,HEESOM,unknown,2007-12-30,1 FORRESTER STREET,BRIGG,City,district,State,DN20 8LR,UK,2019-12-13,2033-01-01 +Valid_NHS,9732009225,BERTHA,BRENT,female,2007-12-30,44 ALGERNON STREET,GRIMSBY,City,district,State,DN32 9QT,UK,2009-07-16,2033-01-01 +Valid_NHS,5996610434,CLEORA,FRAMI,female,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-04-03,2033-01-01 +Valid_NHS,9731528784,JULIE,DEACY,female,2007-12-30,16 HEWETT AVENUE,CAVERSHAM,City,district,State,RG4 7EA,UK,2018-01-21,2033-01-01 +Valid_NHS,5996026643,VICTORIA,DANIEL,female,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-02-05,2033-01-01 +Valid_NHS,5994400971,VAL,PHILLIPS,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-09-04,2033-01-01 +Valid_NHS,5994104973,DEANE,NIELSEN,male,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-08-17,2033-01-01 +Valid_NHS,5993556072,LANGLEY,UDDIN,male,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-06-19,2033-01-01 +Valid_NHS,5992986936,PEGGY,HANE,male,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-03-08,2033-01-01 +Valid_NHS,5992549188,RYDER,LEANNON,male,2007-12-30,581 Lesley Mount SouthStacey,GB,City,district,State,CR9 2BY,UK,2022-12-12,2033-01-01 +Valid_NHS,9473190344,JAKKI,FAITH,female,2007-12-30,3 FAIRWEATHER MEWS,BRADFORD,City,district,State,BD8 0JT,UK,2016-02-08,2033-01-01 +Valid_NHS,9725622006,JEANNE,CHEW,female,2007-12-30,1 SURREY COURT,GRIMSBY,City,district,State,DN32 7JQ,UK,2016-05-10,2033-01-01 +Valid_NHS,9461119232,PANCRAS,KEAST,male,2007-12-30,10 SWAINBY ROAD,TRIMDON,City,district,State,TS29 6JY,UK,2013-12-22,2033-01-01 +Valid_NHS,9658494560,SANDRA,MOODIE,female,2007-12-30,11 BRANKSOME CHINE AVENUE,HASLAND,City,district,State,S41 0PX,UK,2011-08-28,2033-01-01 +Valid_NHS,9694401798,RHYS,HOWDEN,male,2007-12-30,REAR OF 21-23,SEA VIEW STREET,City,district,State,DN35 8EU,UK,2012-03-06,2033-01-01 +Valid_NHS,9460954294,MADALYN,LEE-ORLOPP,female,2007-12-30,BINN VILLA,BINNS LANE,City,district,State,HD9 3JT,UK,2013-12-22,2033-01-01 +Valid_NHS,9725681665,MARTHA,NOLAN,female,2007-12-30,25 LADY FRANCES CRESCENT,CLEETHORPES,City,district,State,DN35 9JX,UK,2008-12-02,2033-01-01 +Valid_NHS,9471117844,DEEANN,SUKUMARAN,female,2007-12-30,HALLIWELL DENE HALL,HEXHAM,City,district,State,NE46 1HW,UK,2016-02-08,2033-01-01 +Valid_NHS,9691429756,SIAN,BROOKE,female,2007-12-30,MYHOLME,COLLEGE ROAD,City,district,State,DN40 3PN,UK,2018-02-21,2033-01-01 +Valid_NHS,9726036208,GERALD,DOLBY,male,2007-12-30,ASHMORE HOUSE,THIRD LANE,City,district,State,DN37 0QU,UK,2011-02-27,2033-01-01 +Valid_NHS,9727119298,DEREK,GETTY,male,2007-12-30,ST. JOHNS COURT,WEST LANE,City,district,State,NE12 7AF,UK,2012-10-26,2033-01-01 +Valid_NHS,9490466514,HANNAH,DAULBY,female,2007-12-30,100 PENSHURST ROAD,CLEETHORPES,City,district,State,DN35 9EN,UK,2008-11-25,2033-01-01 +Valid_NHS,9693351207,ROSE,OSZ-MAY-ALLOC-UNIQUE,female,2007-12-30,10 NORTH STREET,WEST BUTTERWICK,City,district,State,DN17 3JW,UK,2015-03-23,2033-01-01 +Valid_NHS,9694603382,CARRIE,SAVVIN,unknown,2007-12-30,1 CROSLAND ROAD,GRIMSBY,City,district,State,DN37 9DZ,UK,2013-01-13,2033-01-01 +Valid_NHS,9650928480,ELI,SELLY,male,2007-12-30,154 LANGWORTHY ROAD,SALFORD,City,district,State,M6 5PN,UK,2008-12-03,2033-01-01 +Valid_NHS,9726326818,TERRY,NEWBY,male,2007-12-30,1 SHAFTESBURY AVENUE,GRIMSBY,City,district,State,DN34 4AE,UK,2013-09-19,2033-01-01 +Valid_NHS,9486052115,OLAF,GAFNEY,male,2007-12-30,SUMMER HILL COTTAGE,HAWKSHEAD HILL,City,district,State,LA22 0PP,UK,2010-08-11,2033-01-01 +Valid_NHS,9460995098,DENNY,MARTLEW-STONE,unknown,2007-12-30,166 LEEDS ROAD,KIPPAX,City,district,State,LS25 7EL,UK,2013-12-22,2033-01-01 +Valid_NHS,9691210290,MILES,GRAVES,male,2007-12-30,22 ROTHBURY ROAD,SCUNTHORPE,City,district,State,DN17 1EY,UK,2011-12-09,2033-01-01 +Valid_NHS,9693974360,SHAKUNTALA,NEELA,female,2007-12-30,2 SLEDMERE PLACE,LEEDS,City,district,State,LS14 5DX,UK,2020-12-15,2033-01-01 +Valid_NHS,9468044416,CYRIL,FERNS,male,2007-12-30,10 BAILEY WELLS AVENUE,BRADFORD,City,district,State,BD5 9EA,UK,2009-06-15,2033-01-01 +Valid_NHS,9658132332,IOLA,BRYANS,unknown,2007-12-30,10 WOODLAND COURT,LEEDS,City,district,State,LS8 4DE,UK,2011-10-10,2033-01-01 +Valid_NHS,9461488939,ALTERED,GUYVER,female,2007-12-30,1 THE FAIRWAY,WEST ELLA,City,district,State,HU10 7SA,UK,2013-12-22,2033-01-01 +Valid_NHS,9652644072,GEORGE,SIMM,male,2007-12-30,WEST PETEREL FIELD,DIPTON MILL ROAD,City,district,State,NE46 2JT,UK,2012-06-11,2033-01-01 +Valid_NHS,9657717256,SCOTT,WINTON,male,2007-12-30,1 GUNBY ROAD,SCUNTHORPE,City,district,State,DN17 2ET,UK,2014-11-27,2033-01-01 +Valid_NHS,9484909191,MURIEL,ONEIL,female,2007-12-30,ST. LEONARDS COURT,ALFRED STREET,City,district,State,LA1 1FD,UK,2013-12-23,2033-01-01 +Valid_NHS,9674983333,RHODA,KELLY,female,2007-12-30,1 CHUDLEIGH CRESCENT,ILFORD,City,district,State,IG3 9AT,UK,2015-03-13,2033-01-01 +Valid_NHS,9686676945,MARA,BEYER,female,2007-12-30,11 GEORGE STREET,SCUNTHORPE,City,district,State,DN15 6BG,UK,2011-06-05,2033-01-01 +Valid_NHS,9694448298,LEE,STREET,male,2007-12-30,1 COPE STREET,GRIMSBY,City,district,State,DN32 7FB,UK,2019-05-05,2033-01-01 +Valid_NHS,9465235555,JESSA,AGA,female,2007-12-30,1 GREENGATE CRESCENT,EPWORTH,City,district,State,LS1 4HR,UK,2015-04-14,2033-01-01 +Valid_NHS,9690722530,TONIA,MONTACK,female,2007-12-30,10 ST. HYBALDS GROVE,SCAWBY,City,district,State,DN20 9DG,UK,2008-11-26,2033-01-01 +Valid_NHS,9480680912,GAYNOR,EVANS,female,2007-12-30,104 WINDERMERE ROAD,KENDAL,City,district,State,LA9 5EZ,UK,2012-06-30,2033-01-01 +Valid_NHS,9692849708,ESME,LEVY,female,2007-12-30,1 THE BUTTS,STEEPLE ASHTON,City,district,State,BA14 6ES,UK,2011-02-26,2033-01-01 +Valid_NHS,9725852958,JUJHAR,KOLAR,male,2007-12-30,37 RAILWAY STREET,GRIMSBY,City,district,State,DN32 7BN,UK,2014-05-09,2033-01-01 +Valid_NHS,9461207727,JACQUELINE,ANSCOMBE,female,2007-12-30,10 NORTHFIELD CLOSE,WOMERSLEY,City,district,State,DN6 9AB,UK,2013-12-22,2033-01-01 +Valid_NHS,9692521273,SHABEEBA,AMANPOUR,female,2007-12-30,11 WILLOW DRIVE,BARTON-UPON-HUMBER,City,district,State,DN18 5HR,UK,2012-06-23,2033-01-01 +Valid_NHS,9464107308,SHEVON,KALLARACKEL,female,2007-12-30,SHRAHEEN,NORTH MOOR ROAD,City,district,State,DN17 3PX,UK,2007-07-12,2033-01-01 +Valid_NHS,9694230640,LUCY,MULLIN,unknown,2007-12-30,HOMELANDS,SWINSTER LANE,City,district,State,DN40 3NR,UK,2009-03-16,2033-01-01 +Valid_NHS,9461430264,GEORGENE,APPLEBY,female,2007-12-30,CAMBRAI,SUCCESS ROAD,City,district,State,DH4 4TJ,UK,2013-12-22,2033-01-01 +Valid_NHS,9693842332,GLORIA,FILES,female,2007-12-30,13 KING STREET,WINTERTON,City,district,State,DN15 9TP,UK,2017-10-12,2033-01-01 +Valid_NHS,9725967402,ENIDE,MADDOX,female,2007-12-30,37 ASHBY ROAD,CLEETHORPES,City,district,State,DN35 9PH,UK,2016-06-28,2033-01-01 +Valid_NHS,9727094104,ENID,DALE,female,2007-12-30,10 GOSFORTH PARK VILLAS,NORTH GOSFORTH,City,district,State,NE13 6PP,UK,2011-01-07,2033-01-01 +Valid_NHS,9481163520,JONAS,FOWLER,male,2007-12-30,CHARTER HOUSE,BARTON LANE,City,district,State,M30 0HA,UK,2008-07-08,2033-01-01 +Valid_NHS,9457819705,HARRIETTA,TULLOCH,unknown,2007-12-30,10 SHAW LEYS,YEADON,City,district,State,LS19 7LA,UK,2013-12-22,2033-01-01 +Valid_NHS,9480466252,HELENA,LAKE,unknown,2007-12-30,35B,VICTORIA STREET,City,district,State,WV1 3PW,UK,2016-09-16,2033-01-01 +Valid_NHS,5992592474,HECTOR,MCKENZIE,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-12-30,2033-01-01 +Valid_NHS,5992581766,NANCI,BOYER,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-12-30,2033-01-01 +Valid_NHS,5992590412,VINNIE,PRICE,female,2007-12-30,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4GZ,UK,2022-12-30,2033-01-01 +Valid_NHS,5992591133,CARLTON,ORN,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-12-30,2033-01-01 +Valid_NHS,5992635696,GERALD,TREUTEL,male,2007-12-30,Bad Oyenhausen,notgiven,City,district,State,ZZ99 4QZ,UK,2023-01-09,2033-01-01 +Valid_NHS,5992622888,MERCER,ADAMS,male,2007-12-30,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4LZ,UK,2023-01-10,2033-01-01 +Valid_NHS,5992655689,NIKKI,KERR,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-01-13,2033-01-01 +Valid_NHS,5992811184,ELVIN,QUIRKE,male,2007-12-30,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4LZ,UK,2023-02-09,2033-01-01 +Valid_NHS,5992818448,AUGUSTINA,DICKENS,female,2007-12-30,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4YZ,UK,2023-02-09,2033-01-01 +Valid_NHS,5993011513,FAY,YOUNG,male,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-03-10,2033-01-01 +Valid_NHS,5993006161,VIVIEN,NIELSEN,male,2007-12-30,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4QZ,UK,2023-03-10,2033-01-01 +Valid_NHS,9728537646,JIANGUO,QIN,male,2007-12-30,13 PARKINSON AVENUE,SCUNTHORPE,City,district,State,DN15 7JY,UK,2017-11-29,2033-01-01 +Valid_NHS,9728619316,ALICE,FAWKES,female,2007-12-30,MOWBRAY HOUSE,THE VALE,City,district,State,TS4 2UQ,UK,2010-08-15,2033-01-01 +Valid_NHS,5994480436,ORIEL,DAVENPORT,male,2007-12-30,01,Moore,City,district,State,N8 7RE,UK,2023-09-07,2033-01-01 +Valid_NHS,9729789002,ROSE,SAXTON,female,2007-12-30,RAVENTHORPE LODGE,BRIGG ROAD,City,district,State,DN16 3RJ,UK,2021-04-09,2033-01-01 +Valid_NHS,9730040788,NATHAN,CURLEY,male,2007-12-30,10 DEVONSHIRE ROAD,SCUNTHORPE,City,district,State,DN17 1ES,UK,2012-05-03,2033-01-01 +Valid_NHS,5995521330,GABI,SMITH,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-12-08,2033-01-01 +Valid_NHS,5996169991,CONCEPTION,MURRAY,male,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2024-02-16,2033-01-01 +Valid_NHS,5996527030,YVONNE,JACKSON,female,2007-12-30,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-03-21,2033-01-01 +Valid_NHS,5997186105,MARLOWE,AYERS,male,2007-12-30,01,Moore,City,district,State,N8 7RE,UK,2024-06-20,2033-01-01 +Valid_NHS,9732749393,CAROL,HORNBY,female,2007-12-30,10 MONTROSE STREET,SCUNTHORPE,City,district,State,DN16 1LF,UK,2012-12-05,2033-01-01 +Valid_NHS,5998315391,ELINA,SANFORD,male,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-25,2033-01-01 +Valid_NHS,5998469828,ZARA,LEE,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-10-14,2033-01-01 +Valid_NHS,5999038383,BILLYE,DOYLE,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-12-30,2033-01-01 +Valid_NHS,5999036917,COLLEEN,MURPHY,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-12-30,2033-01-01 +Valid_NHS,5999023556,EDYTHE,BARTON,female,2007-12-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-12-30,2033-01-01 +Valid_NHS,9734932357,GAYNOR,MORTON,female,2009-09-23,1 BLUEBELL CLOSE,SCUNTHORPE,City,district,State,DN15 6BS,UK,2012-07-16,2033-01-01 +Valid_NHS,9000006481,ULI,STAFFORD,female,2009-09-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2025-03-13,2033-01-01 +Valid_NHS,9734782479,LUCIEN,LAVER,male,2009-09-23,10 ASHLIN COURT,MESSINGHAM,City,district,State,DN17 3TB,UK,2015-10-22,2033-01-01 +Valid_NHS,5999125758,ULI,HALFORD,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-01-10,2033-01-01 +Valid_NHS,5998989228,AIDAN,DRAPER,male,2009-09-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2024-12-23,2033-01-01 +Valid_NHS,5998830261,NIKKI,QUIRKE,male,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-11-14,2033-01-01 +Valid_NHS,9733630853,GLORIA,DOPSON,female,2009-09-23,1 RHODESIA ROAD,LIVERPOOL,City,district,State,L9 9BS,UK,2016-06-27,2033-01-01 +Valid_NHS,9733627240,SHERI,HAND,female,2009-09-23,12 ASHWOOD CLOSE,BURTON-UPON-STATHER,City,district,State,DN15 9WA,UK,2010-08-10,2033-01-01 +Valid_NHS,5998291328,ORIEL,KANE,male,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-24,2033-01-01 +Valid_NHS,5996955148,ELOUISE,WILLMS,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-05-29,2033-01-01 +Valid_NHS,9730274258,XIAOJING,JEHNG,female,2009-09-23,32 BUTTERWICK ROAD,MESSINGHAM,City,district,State,DN17 3PB,UK,2009-12-28,2033-01-01 +Valid_NHS,5994201928,OAKLEY,PHILLIPS,male,2009-09-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-08-24,2033-01-01 +Valid_NHS,5994291404,GABI,WILLIAMS,male,2009-09-23,23 Warwick Avenue Oakham,NY,City,district,State,BA21 4BH,UK,2023-08-30,2033-01-01 +Valid_NHS,5994040339,KELSEY,UPTON,male,2009-09-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-08-14,2033-01-01 +Valid_NHS,5993878843,HOLLIS,PARKER,male,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-08-01,2033-01-01 +Valid_NHS,5993790431,LANGLEY,DRAPER,male,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-07-21,2033-01-01 +Valid_NHS,5993705493,OLLIE,KAUTZER,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-07-12,2033-01-01 +Valid_NHS,5993486171,ALEX,YOUNG,female,2009-09-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-06-07,2033-01-01 +Valid_NHS,9728208731,GUS,ASHLEY,male,2009-09-23,1 WYVERN CLOSE,CROWLE,City,district,State,DN17 4NW,UK,2016-12-17,2033-01-01 +Valid_NHS,5992643613,HARPER,FELIX,male,2009-09-23,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4QZ,UK,2023-01-12,2033-01-01 +Valid_NHS,9658056334,DWAYNE,JARRETT,male,2009-09-23,1 ELMETE DRIVE,LEEDS,City,district,State,LS8 2LA,UK,2012-01-04,2033-01-01 +Valid_NHS,9486148864,JULIE,LEASK,female,2009-09-23,10 TUDOR SQUARE,DALTON-IN-FURNESS,City,district,State,LA15 8RL,UK,2015-05-21,2033-01-01 +Valid_NHS,9480673614,RUBY,ARDEN,unknown,2009-09-23,1 OAKWOOD CLOSE,LEVENS,City,district,State,LA8 8QD,UK,2016-08-14,2033-01-01 +Valid_NHS,9490431656,HANIYYA,ABDILLAHI,female,2009-09-23,BROW TOP COTTAGE,QUERNMORE,City,district,State,LA2 0QW,UK,2015-01-23,2033-01-01 +Valid_NHS,9460746659,ZILLA,CRUMBLEHOLME,unknown,2009-09-23,1 THE OVAL,BENTON,City,district,State,NE12 9PP,UK,2013-12-22,2033-01-01 +Valid_NHS,9485453760,BASIL,HOMAN,male,2009-09-23,WHITBER,RAVEN GARTH,City,district,State,LA8 9PG,UK,2010-08-10,2033-01-01 +Valid_NHS,9660882157,RUBIN,BOON,male,2009-09-23,10 NORMANBY ROAD,SCUNTHORPE,City,district,State,DN15 6AL,UK,2016-06-12,2033-01-01 +Valid_NHS,9460166660,AVANTI,SOURAV,female,2009-09-23,12 FERRY ROAD,HESSLE,City,district,State,HU13 0DR,UK,2013-12-22,2033-01-01 +Valid_NHS,9490426407,AVRAHAM,ISRAEL,male,2009-09-23,19 WOBORROW ROAD,HEYSHAM,City,district,State,LA3 2PW,UK,2016-12-08,2033-01-01 +Valid_NHS,9651100753,ZERLINDA,ROTH,female,2009-09-23,8 LONGBOW AVENUE,METHLEY,City,district,State,LS26 9LD,UK,2015-03-31,2033-01-01 +Valid_NHS,9658334423,CLIVE,JVH-APR-INVALIDTEL11,male,2009-09-23,49 LILAC AVENUE,SCUNTHORPE,City,district,State,DN16 1JL,UK,2012-07-21,2033-01-01 +Valid_NHS,9651866659,LEE,AYRE,male,2009-09-23,36 CORONATION ROAD SOUTH,HULL,City,district,State,HU5 5QN,UK,2012-04-28,2033-01-01 +Valid_NHS,9691659565,STAN,CONVEY,male,2009-09-23,2 CROSS STREET,WEST HALTON,City,district,State,DN15 9AY,UK,2010-09-21,2033-01-01 +Valid_NHS,9692150941,LEVI,ELAND,male,2009-09-23,1 FOWLER COURT,WINTERTON,City,district,State,DN15 9XQ,UK,2017-05-08,2033-01-01 +Valid_NHS,9457833562,ELEANOR,ALLEYNE,unknown,2009-09-23,1 IVY MOUNT,LEEDS,City,district,State,LS9 9BS,UK,2013-12-22,2033-01-01 +Valid_NHS,9480679728,SHARON,HOAKES,female,2009-09-23,10 LOFTUS HILL,SEDBERGH,City,district,State,LA10 5RX,UK,2012-06-18,2033-01-01 +Valid_NHS,9726031044,HETTY,GYLES,female,2009-09-23,10 MILLER AVENUE,GRIMSBY,City,district,State,DN32 8JW,UK,2013-03-24,2033-01-01 +Valid_NHS,9470076400,GODWIN,DICEMBRE,unknown,2009-09-23,HOLE HOUSE,HIGH WRAY,City,district,State,LA22 0JF,UK,2016-02-08,2033-01-01 +Valid_NHS,9726216265,DON,HURLEY,male,2009-09-23,1 SIDNEY COURT,CLEETHORPES,City,district,State,DN35 7PJ,UK,2019-05-05,2033-01-01 +Valid_NHS,9460098169,LEEANN,MEGAFU,female,2009-09-23,39 JERVIS COURT,SUTTON ON DERWENT,City,district,State,YO41 4JX,UK,2013-12-22,2033-01-01 +Valid_NHS,9692150542,ALBERT,ADAMSEU,male,2009-09-23,1 WHITEHILL QUAY,LEEDS,City,district,State,LS1 4HY,UK,2018-05-10,2033-01-01 +Valid_NHS,9694227046,CALVIN,DAWKIN,male,2009-09-23,21 WEST END ROAD,EPWORTH,City,district,State,DN9 1LA,UK,2019-04-13,2033-01-01 +Valid_NHS,9726235928,JOSH,SWANN,male,2009-09-23,12 TRINITY ROAD,CLEETHORPES,City,district,State,DN35 8UQ,UK,2015-10-13,2033-01-01 +Valid_NHS,9490423343,ZHI,OU,unknown,2009-09-23,7 SLATER STREET,DALTON-IN-FURNESS,City,district,State,LA15 8SR,UK,2016-02-02,2033-01-01 +Valid_NHS,9651815566,CASEY,ALDRED,female,2009-09-23,12 ST. NICHOLAS AVENUE,HULL,City,district,State,HU4 7AJ,UK,2016-08-06,2033-01-01 +Valid_NHS,9471184614,WIL,TWEMLOW,unknown,2009-09-23,OLD HALL LODGE,MAIN ROAD,City,district,State,HU12 0UP,UK,2016-02-08,2033-01-01 +Valid_NHS,9726452139,DONALD,DAVIS,male,2009-09-23,44 ANCASTER AVENUE,GRIMSBY,City,district,State,DN33 3LW,UK,2013-01-22,2033-01-01 +Valid_NHS,9457944798,DALY,MACMURDOCH,male,2009-09-23,148 PENTLAND AVENUE,BILLINGHAM,City,district,State,TS23 2RE,UK,2013-12-22,2033-01-01 +Valid_NHS,9469997093,RHETT,MAUGHAN,unknown,2009-09-23,10 ROMNEY ROAD,KENDAL,City,district,State,LA9 5QR,UK,2016-02-08,2033-01-01 +Valid_NHS,9657863023,ZELMA,PARKS,unknown,2009-09-23,2 DEVON CLOSE,LEEDS,City,district,State,LS2 9AD,UK,2016-02-03,2033-01-01 +Valid_NHS,9692704246,CHLOE,BEGGS,female,2009-09-23,2 ERMINE STREET,BROUGHTON,City,district,State,DN20 0DQ,UK,2017-10-27,2033-01-01 +Valid_NHS,9652718076,SHARON,ROTDEN,female,2009-09-23,1 CHAPEL LANE,ALNMOUTH,City,district,State,NE66 2RR,UK,2013-03-14,2033-01-01 +Valid_NHS,9485409273,SONIA,EGAN,unknown,2009-09-23,2 BACK FOX STREET,SWARTHMOOR,City,district,State,LA12 0JH,UK,2011-10-16,2033-01-01 +Valid_NHS,9658438202,RUBIN,CORBY,male,2009-09-23,6 DE LA POLE AVENUE,HULL,City,district,State,HU3 6RX,UK,2017-02-23,2033-01-01 +Valid_NHS,9726290104,ASHLEY,LEES,female,2009-09-23,10 MONTAGUE STREET,CLEETHORPES,City,district,State,DN35 7AP,UK,2010-12-10,2033-01-01 +Valid_NHS,9485317425,RHONA,DAWN,female,2009-09-23,2 MEADUP COURT,MORECAMBE,City,district,State,LA3 3NS,UK,2014-08-13,2033-01-01 +Valid_NHS,9657139899,CECIL,GIG-NOV-ALLOC-ALPHA_MULTI,male,2009-09-23,100 RAVENDALE STREET SOUTH,SCUNTHORPE,City,district,State,DN15 6QG,UK,2011-06-06,2033-01-01 +Valid_NHS,5990078773,HANNAH,BURDEN,female,2009-09-23,13 Astley Walk,Temple Herdewyke,City,district,State,CV47 2UN,UK,2009-09-23,2033-01-01 +Valid_NHS,9490400874,AMANDA,ROWLEY,unknown,2009-09-23,VICTORIA WHARF,ST. GEORGES QUAY,City,district,State,LA1 1GA,UK,2014-05-04,2033-01-01 +Valid_NHS,9650390618,LUCIEN,OLSEN,male,2009-09-23,10 FERN BANK,LANCASTER,City,district,State,LA1 4TT,UK,2012-11-28,2033-01-01 +Valid_NHS,9657159024,JONAS,SPARKS,male,2009-09-23,2 RUDGATE MEWS,THORP ARCH,City,district,State,LS23 7RB,UK,2017-09-16,2033-01-01 +Valid_NHS,9460915884,KATHERINA,MAJEKODUNMI,female,2009-09-23,1 FRONT STREET,TUDHOE COLLIERY,City,district,State,DL16 6TG,UK,2013-12-22,2033-01-01 +Valid_NHS,9461187114,DENICE,GILLAM,female,2009-09-23,COWSTONE GILL HOUSE,WALDEN,City,district,State,DL8 4LE,UK,2013-12-22,2033-01-01 +Valid_NHS,9461450729,MYRTIE,WARDHAUGH,female,2009-09-23,2 COUNCIL HOUSES,WELL,City,district,State,DL8 2QB,UK,2013-12-22,2033-01-01 +Valid_NHS,9693004426,NANCIE,GOSS,female,2009-09-23,15 MILKWOOD ROAD,LONDON,City,district,State,SE24 0HX,UK,2010-03-07,2033-01-01 +Valid_NHS,9462714436,CLARE,DUDLEY,female,2009-09-23,11 OGMORE CLOSE,TILEHURST,City,district,State,RG30 4SX,UK,2014-05-28,2033-01-01 +Valid_NHS,9691610264,MAREE,JORDEN,unknown,2009-09-23,10 SCAWBY ROAD,SCUNTHORPE,City,district,State,DN17 2HZ,UK,2017-10-06,2033-01-01 +Valid_NHS,9726100488,ISAAC,BRAHAM,male,2009-09-23,1 HOMERTON PLACE,GRIMSBY,City,district,State,DN34 5XQ,UK,2016-01-18,2033-01-01 +Valid_NHS,9485299842,KATIE,HALE,female,2009-09-23,10 BEACH CRESCENT,WALNEY,City,district,State,LA14 3YA,UK,2013-03-13,2033-01-01 +Valid_NHS,9725543289,TAMMY,GOULD,female,2009-09-23,10 WESTERN OUTWAY,GRIMSBY,City,district,State,DN34 5HE,UK,2013-03-14,2033-01-01 +Valid_NHS,9692962733,ISAAC,DUCROW,male,2009-09-23,39 HARRIS AVENUE,LOWESTOFT,City,district,State,NR32 4BD,UK,2009-09-26,2033-01-01 +Valid_NHS,9694090660,JOANNA,MELBIN,female,2009-09-23,1 CLAYTON WAY,BLACKBURN,City,district,State,BB2 4FZ,UK,2019-06-11,2033-01-01 +Valid_NHS,9460125964,NETTA,KENNISH,female,2009-09-23,13 FORGE LANE,KIRKBY FLEETHAM,City,district,State,DL7 0SA,UK,2013-12-22,2033-01-01 +Valid_NHS,9466399322,BRISTOL,DOWSON,male,2009-09-23,13 WELBECK ROAD,GRIMSBY,City,district,State,DN34 5NJ,UK,2015-08-03,2033-01-01 +Valid_NHS,9475763643,EVELYN,SWENN,female,2009-09-23,11 VERNON PARK,GALGATE,City,district,State,LA2 0LU,UK,2014-08-08,2033-01-01 +Valid_NHS,9485349378,BAILEE,SHIEL,female,2009-09-23,2 DYKES LANE,YEALAND CONYERS,City,district,State,LA5 9SP,UK,2016-05-06,2033-01-01 +Valid_NHS,5992666907,ODESSA,KUHLMAN,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-01-16,2033-01-01 +Valid_NHS,5992672338,YVONNE,WALKER,unknown,2009-09-23,01,Moore,City,district,State,N8 7RE,UK,2023-01-16,2033-01-01 +Valid_NHS,9727903002,JAYNE,HYCOCK,female,2009-09-23,35 WOODFIELD ROAD,SPARKBROOK,City,district,State,B12 8UJ,UK,2020-08-16,2033-01-01 +Valid_NHS,5992819770,BRETT,KANE,male,2009-09-23,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 4QZ,UK,2023-02-10,2033-01-01 +Valid_NHS,5992831886,WYATT,BEDNAR,female,2009-09-23,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 7LZ,UK,2023-02-13,2033-01-01 +Valid_NHS,5993191449,ANGELITA,ANGELITA,female,2009-09-23,95181 Hauck Plaza Newcyril,GB,City,district,State,CR9 2BY,UK,2023-04-11,2033-01-01 +Valid_NHS,9728490143,WARREN,FOOT,male,2009-09-23,3 CHURCH VILLAS,JACKLINS LANE,City,district,State,DN17 4RB,UK,2019-10-07,2033-01-01 +Valid_NHS,5993356731,RICK,POUROS,male,2009-09-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2023-05-15,2033-01-01 +Valid_NHS,5993558318,MARLOWE,UNDERWOOD,female,2009-09-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-06-16,2033-01-01 +Valid_NHS,9730630984,OTTO,SPRATT,male,2009-09-23,NEWLANDS,TOWNSIDE,City,district,State,DN40 3NS,UK,2013-10-20,2033-01-01 +Valid_NHS,9732058188,MINA,HEALEY,unknown,2009-09-23,12 WESLEY STREET,KIRTON LINDSEY,City,district,State,DN21 4PB,UK,2010-08-15,2033-01-01 +Valid_NHS,5998283856,ALFONZO,CARROLL,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998276019,SYLVESTER,ABSHIRE,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998291883,GERTRUDIS,ALTENWERTH,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998261283,ROSAMARIA,PFANNERSTILL,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998276167,ROSALINE,REILLY,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998296621,OLEN,SWIFT,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998274962,CATHERYN,BEDNAR,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998265572,VALENTINE,MILLS,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998261224,SHERMAN,TRANTOW,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998264711,LEANDRA,SMITH,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998277465,OLINDA,PRICE,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998269969,BRAIN,BEAHAN,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998276418,GEORGEANNA,ROBERTS,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998275136,JONAH,HELLER,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998281071,KENNITH,JENKINS,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998264061,PRESTON,JASKOLSKI,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998276531,STERLING,BERGNAUM,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998262948,CLELIA,JACOBSON,female,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-09-23,2033-01-01 +Valid_NHS,5998298748,ADRIAN,PHILLIPS,male,2009-09-23,17 Queensway Manchester,MANCHESTER,City,district,State,AB11 8TQ,UK,2024-09-25,2033-01-01 +Valid_NHS,5998467523,RISA,HOWE,male,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-10-14,2033-01-01 +Valid_NHS,9733615277,ALISON,JOPSON,female,2009-09-23,COLMARDEN,CARRSIDE,City,district,State,DN9 1DX,UK,2018-01-14,2033-01-01 +Valid_NHS,9734075160,YVETTE,MEGINN,female,2009-09-23,1 PASTURE PLACE,LEEDS,City,district,State,LS7 4QU,UK,2021-12-02,2033-01-01 +Valid_NHS,5999407044,MARLOWE,WILLIAMS,male,2009-09-23,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-02-01,2033-01-01 +Valid_NHS,9000181798,ROBIN,JACKSON,male,2008-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000190665,ROMWAN,FELIX,male,2009-03-26,Campus City New Campus 41 Berkeley Road Westbury Park,MANCHESTER,City,district,State,M16 0RA,UK,2025-03-26,2033-01-01 +Valid_NHS,9000191882,DOUGLASS,HODKIEWICZ,male,2008-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9735046032,MATHEW,LAMB,male,2007-12-07,3 WALTON CHASE,THORP ARCH,City,district,State,LS23 7RA,UK,2017-08-23,2033-01-01 +Valid_NHS,9735048515,GINA,MCEWEN,female,2002-03-29,10 SANDHILL MOUNT,LEEDS,City,district,State,LS17 8EQ,UK,2015-09-16,2033-01-01 +Valid_NHS,9735058413,RAY,CLARK,male,2004-10-24,9 THE DELL,BARDSEY,City,district,State,LS17 9DL,UK,2020-07-06,2033-01-01 +Valid_NHS,9000164672,MONARCH,BARAN,male,2000-12-12,Cherish Wealth Management Ltd Malve,New Road,City,district,State,B91 3DL,UK,2025-03-25,2033-01-01 +Valid_NHS,9000175755,GABI,DRAPER,female,2009-04-09,01,Moore,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000178193,JACKIE,STAFFORD,male,2008-09-08,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000191467,HARLEY,GOODMAN,male,2008-09-11,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000194970,REBBECCA,FRIESEN,male,2009-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9735017571,MIKALA,BOWYER,unknown,2008-04-13,1 MONKS WAY,SHIREOAKS,City,district,State,S81 8NE,UK,2021-08-19,2033-01-01 +Valid_NHS,9735019493,PADERAU,LOMMIS,unknown,2009-06-14,1 CLARENDON DRIVE,WORKSOP,City,district,State,S81 7BW,UK,2017-11-12,2033-01-01 +Valid_NHS,9735021137,JENNY,YATES,female,2008-07-27,76 MOORGATE,RETFORD,City,district,State,DN22 6RR,UK,2024-10-18,2033-01-01 +Valid_NHS,9735023571,NATHAN,AQZ-MAR-PDS-ALPHA-ZERO,male,2006-02-04,1 MIRE LANE,SUTTON,City,district,State,DN22 8PX,UK,2019-11-02,2033-01-01 +Valid_NHS,9735029561,EMILIE,BRADY,female,2006-07-25,1 PELFINTAX,WESTWOODSIDE,City,district,State,DN9 2EL,UK,2025-02-14,2033-01-01 +Valid_NHS,9735047373,LIZZY,HOEY,female,2004-07-04,SCHOOL BUNGALOW,DEIGHTON ROAD,City,district,State,LS22 7XL,UK,2019-08-27,2033-01-01 +Valid_NHS,9735057611,RENA,MORROW,female,2001-02-17,42 CHURCH LANE,BARDSEY,City,district,State,LS17 9DS,UK,2022-07-18,2033-01-01 +Valid_NHS,9000184657,BEN,ALTENWERTH,male,2006-03-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000184673,VIRGIL,HAHN,female,2008-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000194210,MARLOWE,RAMSAY,female,2007-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000195330,NIKKI,WELLER,male,2008-07-02,50 Xenia Street Jersey,NE,City,district,State,GU22 0SX,UK,2025-03-26,2033-01-01 +Valid_NHS,9000197619,EMMALINE,MONAHAN,female,2009-04-12,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000198933,EDUARDO,KUHLMAN,male,2007-01-19,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9735043807,LUKE,EVANS,male,2004-12-14,1 WALK HOUSE FARM COTTAGE,WINTERTON,City,district,State,DN15 9RE,UK,2005-04-10,2033-01-01 +Valid_NHS,9735054108,HAZEL,CASEY,female,2003-08-10,5 THE GREEN,THORP ARCH,City,district,State,LS23 7AB,UK,2013-11-29,2033-01-01 +Valid_NHS,9000164400,YASMINE,KANE,male,2001-06-04,14 Notting Hill Gate Oakham,NC,City,district,State,SS8 7HE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000164850,ORIEL,QUINN,female,2008-04-05,01,Moore,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000165415,ADELINA,GOYETTE,male,2009-03-25,02,Test Street,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000169461,CHRISTINA,OSINSKI,male,2009-12-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000171229,ROGELIO,MANTE,female,2003-01-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000173124,ZARA,HALFORD,male,2009-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000175321,ALFRED,BEAHAN,male,2008-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000178134,DEANE,AYERS,female,2008-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000179246,CORTEZ,POWLOWSKI,male,2007-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000191769,BRETT,GUTKOWSKI,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000195241,ALEX,VARDY,female,2000-08-11,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000198194,PAIGE,CLARKE,female,2004-04-27,Romford 3 Carnaby Street Az,ROMFORD,City,district,State,DH4 4QL,UK,2025-03-25,2033-01-01 +Valid_NHS,9735014025,DOREAS,BLAIR,unknown,2006-07-25,3 CORNFIELD CLOSE,CARLTON-IN-LINDRICK,City,district,State,S81 9TF,UK,2014-11-13,2033-01-01 +Valid_NHS,9735020939,ROSS,BOWYER,male,2008-08-18,THE BUNGALOW,MARNHAM ROAD,City,district,State,NG22 0PY,UK,2023-09-03,2033-01-01 +Valid_NHS,9735023555,CLAUDE,AQZ-MAR-PDS-ALPHA-ZERO,male,2003-08-02,1 GODFREYS COURT,WORKSOP,City,district,State,S80 1TT,UK,2023-12-14,2033-01-01 +Valid_NHS,9000181208,TRISTEN,MORGAN,male,2001-06-04,2 Baker Street Edinburgh,NJ,City,district,State,SP1 3RS,UK,2025-03-26,2033-01-01 +Valid_NHS,9000182069,ROBIN,SAUER,male,2004-08-18,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000182212,HOUSTON,ERNSER,female,2008-03-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000186048,NIKKI,HALFORD,female,2007-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000196426,NELL,VARDY,male,2008-03-30,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000197996,GIOVANNI,KULAS,male,2008-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000199336,TRESA,BOTSFORD,male,2007-03-12,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000201284,DENNIS,CORWIN,male,2009-11-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9735045281,ANDREW,NEISH,male,2003-10-19,2 BARROW ROAD,NEW HOLLAND,City,district,State,DN19 7QN,UK,2013-08-18,2033-01-01 +Valid_NHS,9735047578,IKHTIAR,JANDA,female,2007-03-02,102 EAST COMMON LANE,SCUNTHORPE,City,district,State,DN16 1QH,UK,2019-10-06,2033-01-01 +Valid_NHS,9735048531,RAMONA,DORAN,female,2000-05-27,10 DEWAR CLOSE,COLLINGHAM,City,district,State,LS22 5JR,UK,2015-06-04,2033-01-01 +Valid_NHS,9735050331,GERALD,STYCHE,male,2001-06-06,10 AINSTY DRIVE,WETHERBY,City,district,State,LS22 7QW,UK,2007-08-24,2033-01-01 +Valid_NHS,9735052059,ROLAND,DAULBY,male,2007-03-21,12 THE DRIVE,ALWOODLEY,City,district,State,LS17 7QA,UK,2020-03-03,2033-01-01 +Valid_NHS,9735053829,WARREN,DUKE,male,2004-06-19,ASTURA COURT,POTTERNEWTON MOUNT,City,district,State,LS7 2DL,UK,2014-01-19,2033-01-01 +Valid_NHS,9000161673,FORDEN-ROBBEN,WINDERDALE-SMITH,male,2002-01-01,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,M16 0RA,UK,2025-03-25,2033-01-01 +Valid_NHS,9000167523,JORDON,ROGAHN,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000169704,TRISTEN,INGHAM,female,2008-08-31,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000174066,RAGUEL,HAUCK,female,2008-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000176824,ZARA,OLIVIER,male,2001-06-04,31 Earl'S Court Road Kettering,TX,City,district,State,PE29 2NS,UK,2025-03-25,2033-01-01 +Valid_NHS,9000179092,SHAWN,UNDERWOOD,female,2009-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000197732,SONYA,HILPERT,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9735010844,LEWIS,VERDI,male,2009-03-21,1 THE WOODLANDS,STALLINGBOROUGH,City,district,State,DN41 8BH,UK,2009-06-23,2033-01-01 +Valid_NHS,9735013363,STEVE,MELIA,male,2006-04-03,10 GREENACRE ROAD,WORKSOP,City,district,State,S81 0SL,UK,2021-02-03,2033-01-01 +Valid_NHS,9735016028,EDMOND,SMITH,male,2002-05-05,1 LABURNUM ROAD,LANGOLD,City,district,State,S81 9RR,UK,2011-02-18,2033-01-01 +Valid_NHS,9735023628,CALVIN,AQZ-MAR-PDS-ALPHA-ZERO,male,2000-08-16,10 CHURCH LANE,HAYTON,City,district,State,DN22 9LD,UK,2007-08-08,2033-01-01 +Valid_NHS,9735023652,JEREMY,AQZ-MAR-PDS-ALPHA-ZERO,male,2002-12-30,12 KNATON ROAD,CARLTON-IN-LINDRICK,City,district,State,S81 9HQ,UK,2015-06-14,2033-01-01 +Valid_NHS,9000191092,TOM,JACOBS,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000180090,CAROLYN,JASKOLSKI,male,2001-10-29,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000188423,ALFRED,FLATLEY,male,2006-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000194598,SIDNEY,IZZARD,male,2001-06-04,7 Goodge Street Ipswitch,OR,City,district,State,NE44 6AT,UK,2025-03-26,2033-01-01 +Valid_NHS,9000195268,URI,OWEN,female,2009-03-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9735044978,EVAN,FORMBY,male,2004-08-31,43 BURKE STREET,SCUNTHORPE,City,district,State,DN15 6DP,UK,2009-10-18,2033-01-01 +Valid_NHS,9735046016,BASIL,PECK,male,2009-06-27,10 LINGFIELD HILL,LEEDS,City,district,State,LS17 7EH,UK,2020-09-20,2033-01-01 +Valid_NHS,9735046288,HUGH,LEECH,male,2001-10-09,101 GATELAND LANE,LEEDS,City,district,State,LS17 8LW,UK,2025-03-20,2033-01-01 +Valid_NHS,9735053780,RANDY,REED,male,2004-03-28,1 OAKWELL AVENUE,LEEDS,City,district,State,LS8 4AQ,UK,2022-07-12,2033-01-01 +Valid_NHS,9735055341,CALEB,PALMAS,male,2008-06-18,192 HIGH STREET,BOSTON SPA,City,district,State,LS23 6BT,UK,2020-07-13,2033-01-01 +Valid_NHS,9735057255,ESME,BENYON,female,2009-11-19,1 RIEVAULX CLOSE,BOSTON SPA,City,district,State,LS23 6RT,UK,2025-02-15,2033-01-01 +Valid_NHS,9735057581,MONA,SHEA,female,2005-09-10,10 PARK ROAD,BOSTON SPA,City,district,State,LS23 6NH,UK,2010-07-04,2033-01-01 +Valid_NHS,9735059053,WARREN,MANN,male,2001-11-25,11 COWPER STREET,LEEDS,City,district,State,LS7 4DR,UK,2020-02-06,2033-01-01 +Valid_NHS,9000161517,MARGRETT,KIEHN,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000162947,FRANKIE,LAWRENCE,male,2000-08-11,43 Queensland Road Slough,AR,City,district,State,E6 2RJ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000166799,LYNWOOD,CARTER,male,2007-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000167477,VIVIEN,UNDERWOOD,female,2009-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000170559,ARTURO,SMITHAM,female,2008-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000171814,LAVERNE,KUHIC,female,2003-07-19,Address Line 1,Address Line 2,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000172462,JERRELL,HUELS,male,2009-03-25,02,Test Street,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000176433,BAILEY,STAFFORD,female,2001-08-11,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 2CZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000182999,CHUNG,RAU,male,2003-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9735011042,RAY,COOMER,male,2001-05-31,32 TINTERN WALK,GRIMSBY,City,district,State,DN37 9JG,UK,2012-03-26,2033-01-01 +Valid_NHS,9735016737,WENDY,MATTIX,female,2004-08-25,10 ASH CROFT,RETFORD,City,district,State,DN22 7FB,UK,2008-12-03,2033-01-01 +Valid_NHS,9000191823,ELVIN,UNDERWOOD,male,2008-05-02,5 Edgware Road Dartford,DARTFORD,City,district,State,UB2 5NR,UK,2025-03-26,2033-01-01 +Valid_NHS,9000193060,LAVERA,GOTTLIEB,male,2009-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000193737,SIDNEY,QUIRKE,female,2007-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000200334,ALEX,VAUGHAN,male,2000-08-11,22 Victoria Drive Ilfracombe,AZ,City,district,State,TF9 1LJ,UK,2025-03-27,2033-01-01 +Valid_NHS,9735043602,IRVING,HAROLD,male,2007-07-18,1 WYVERN CLOSE,CROWLE,City,district,State,DN17 4NW,UK,2008-01-26,2033-01-01 +Valid_NHS,9735043629,BART,RANKIN,male,2008-10-06,28 AUDERN ROAD,SCUNTHORPE,City,district,State,DN16 3LJ,UK,2017-06-26,2033-01-01 +Valid_NHS,9735046261,BRAD,SWAIN,male,2007-03-07,43 HARRIET STREET,LEEDS,City,district,State,LS7 4DF,UK,2016-08-08,2033-01-01 +Valid_NHS,9735055570,JONAS,MENEAR,male,2001-05-28,1 BECKHILL GROVE,LEEDS,City,district,State,LS7 2RX,UK,2019-06-26,2033-01-01 +Valid_NHS,9735057239,AMY,SWIFT,female,2009-11-22,4 BELVEDERE AVENUE,ALWOODLEY,City,district,State,LS17 8BW,UK,2013-04-11,2033-01-01 +Valid_NHS,9000168104,ROMWAN,OLIVIER,male,2008-04-01,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,ZZ99 6CZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000170761,ELLIS,PHILLIPS,female,2009-05-27,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000174139,RODERICK,SCHINNER,male,2008-01-15,Clapton Station,High Street Mall,City,district,State,SW1H 0BT,UK,2025-03-25,2033-01-01 +Valid_NHS,9000174821,ALEX,CROFT,female,2007-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000190746,KELSEY,YOUNG,male,2008-10-09,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000194466,CLAUDE,KANE,male,2009-11-15,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000199085,KIESHA,TRANTOW,male,2006-08-03,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9735011387,CERI,HOR,female,2002-05-06,40 BRAMHALL STREET,CLEETHORPES,City,district,State,DN35 7QY,UK,2020-09-26,2033-01-01 +Valid_NHS,9735015269,TAMSYN,WAITE,female,2007-08-01,11 SANDYMOUNT,HARWORTH,City,district,State,DN11 8QQ,UK,2016-08-22,2033-01-01 +Valid_NHS,9735021153,MARCIA,RAYNER,female,2009-02-07,1 STUBBING LANE,WORKSOP,City,district,State,S80 1ND,UK,2023-05-02,2033-01-01 +Valid_NHS,9735032406,LARRY,READON,male,2004-12-22,3 REGENT DRIVE,CROWLE,City,district,State,DN17 4TB,UK,2023-03-10,2033-01-01 +Valid_NHS,9735016133,CHERYL,GALVIN,female,2009-01-01,2 TURNERS CROFT,NORTH LEVERTON,City,district,State,DN22 0BG,UK,2011-05-08,2033-01-01 +Valid_NHS,9000187354,WHITNEY,UPTON,male,2009-10-05,London 47 Union Walk Nc,NC,City,district,State,BL7 9YE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000194393,ZOE,MANSELL,male,2001-08-11,Widnes 15 Old Street Ok,OK,City,district,State,WC1H 0PN,UK,2025-03-26,2033-01-01 +Valid_NHS,9000195209,SIDNEY,VAUGHAN,male,2007-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000195659,LUCA,LITT,male,2004-10-26,Sydenham House,Mill Ct,City,district,State,TN24 8DN,UK,2025-03-26,2033-01-01 +Valid_NHS,9000198992,VIVIEN,ADAMS,male,2007-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000199980,LANGLEY,CROFT,male,2009-03-26,Campus City New Campus 41 Berkeley Road Westbury Park,MANCHESTER,City,district,State,M16 0RA,UK,2025-03-26,2033-01-01 +Valid_NHS,9000200954,BAILEY,WALKER,male,2001-06-04,12 Latimer Place Oakham,SD,City,district,State,TN16 1RT,UK,2025-03-27,2033-01-01 +Valid_NHS,9000204461,AIDAN,MANSELL,male,2001-06-04,13 Mortimer Square Slough,KY,City,district,State,W1U 4PF,UK,2025-03-26,2033-01-01 +Valid_NHS,9000208033,AINSLEY,HALFORD,male,2001-06-04,50 Xenia Street Oakham,MA,City,district,State,TS28 5NP,UK,2025-03-27,2033-01-01 +Valid_NHS,9735048191,LUCIEN,GYLES,male,2000-02-09,ASH LEA,LOWER LANGWITH,City,district,State,LS22 5BX,UK,2016-06-05,2033-01-01 +Valid_NHS,9735052296,MARIA,HOARE,female,2003-04-12,3 PARKWOOD GARDENS,ROUNDHAY,City,district,State,LS8 1JR,UK,2017-02-08,2033-01-01 +Valid_NHS,9735053160,HECTOR,PEENEY,male,2005-10-10,10 GUNTER ROAD,WETHERBY,City,district,State,LS22 6JZ,UK,2012-10-02,2033-01-01 +Valid_NHS,9735057336,LUCAS,ASTLEY,male,2005-10-26,1 WELL HOUSE DRIVE,LEEDS,City,district,State,LS8 4BX,UK,2024-10-04,2033-01-01 +Valid_NHS,9735057603,JOANNE,FLYNN,female,2000-03-10,4 LINGFIELD APPROACH,LEEDS,City,district,State,LS17 6BY,UK,2024-09-05,2033-01-01 +Valid_NHS,9735059010,PETER,EYRES,male,2003-08-30,10 PRIMLEY PARK AVENUE,LEEDS,City,district,State,LS17 7JA,UK,2009-11-08,2033-01-01 +Valid_NHS,9000160642,GALE,CLARKE,male,2009-07-16,24 Xavier Street Ilfracombe,ILFRACOMBE,City,district,State,IG2 6TQ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000161061,SU,CHAMPLIN,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000167590,REY,MURAZIK,male,2005-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000169224,HILTON,BAUMBACH,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000169763,GARY,BARTELL,male,2000-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000174279,KING,CHAMPLIN,male,2008-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000177138,SKYE,KIHN,female,2008-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000177391,DALE,ELSON,male,2009-11-01,01,Moore,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000182980,MYRIAM,JAST,male,2000-06-04,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9735029030,RAMON,BIRD,male,2009-09-22,4 KINGSDALE,SCUNTHORPE,City,district,State,DN17 2NT,UK,2022-06-28,2033-01-01 +Valid_NHS,9000180449,WESTLEY,KERR,male,2001-06-04,40 North End Road Swanage,NM,City,district,State,TQ6 9QU,UK,2025-03-26,2033-01-01 +Valid_NHS,9000183545,LUCINA,ALTENWERTH,female,2009-07-28,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000186129,GILBERT,HAGENES,male,2006-03-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000163927,RUBIN,DAVIS,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000180260,ELLIS,BAILEY,male,2000-08-11,24 Xavier Street,Verwood,City,district,State,ZZ99 9GZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000192110,TRISTEN,PHILLIPS,male,2008-10-03,Slough 32 Fenchurch Street Slough,GERMANY,City,district,State,ZZ99 4GZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000193249,SIDNEY,SIMONIS,female,2008-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000197260,AINSLEY,ELSON,female,2007-03-26,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000199212,SIDNEY,VENABLES,male,2000-08-11,Ashford 45 Springfield Rise Ar,AR,City,district,State,HX3 9LB,UK,2025-03-26,2033-01-01 +Valid_NHS,9000199344,ELVIRA,CRUICKSHANK,male,2005-04-08,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-26,2033-01-01 +Valid_NHS,9000207487,GABI,ELSON,male,2001-08-11,47 Union Walk Hatfield,NH,City,district,State,DY3 2DZ,UK,2025-03-27,2033-01-01 +Valid_NHS,9735046210,CECIL,SIM,male,2005-12-25,WHITEGATES,DOWKELL LANE,City,district,State,LS23 7AH,UK,2007-05-10,2033-01-01 +Valid_NHS,9735050269,RAMONA,DEEGAN,female,2007-11-26,1 MONTAGU VIEW,LEEDS,City,district,State,LS8 2RH,UK,2013-01-14,2033-01-01 +Valid_NHS,9735053764,PEGGY,GORE,female,2008-01-01,1 BARLEYFIELDS TERRACE,WETHERBY,City,district,State,LS22 6PW,UK,2019-07-06,2033-01-01 +Valid_NHS,9735054949,HALEY,FRIAR,female,2003-02-13,1 SHADWELL WALK,LEEDS,City,district,State,LS17 6EQ,UK,2024-11-08,2033-01-01 +Valid_NHS,9735057093,RAY,BROOKS,male,2008-10-22,6 CARAWAY COURT,MEANWOOD,City,district,State,LS6 4RY,UK,2016-03-11,2033-01-01 +Valid_NHS,9000161568,LANELLE,GORCZANY,female,2008-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000166225,WELDON,SCHUPPE,male,2009-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000167302,HARRIS,WALKER,female,2008-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000169283,MARCOS,DIBBERT,male,2008-10-09,02,Test Street,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000174376,VAL,ADAMS,male,2009-05-04,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000178967,MAIRA,FISHER,male,2008-03-25,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000180147,VAL,RAMSAY,female,2002-02-11,Rothwell 6 Finchley Road Ri,ROTHWELL,City,district,State,TQ13 9HH,UK,2025-03-25,2033-01-01 +Valid_NHS,9000187699,PENELOPE,BAILEY,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000188822,VAUGHN,ZEMLAK,male,2001-05-23,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9735015684,MARTIN,ORMSBY,male,2008-06-08,ALDERLEY,HILL ROAD,City,district,State,DN11 8JR,UK,2011-05-15,2033-01-01 +Valid_NHS,9735019124,KERRY,CRONE,female,2006-07-23,QUEENS COURT,QUEEN STREET,City,district,State,DN22 7DL,UK,2011-03-23,2033-01-01 +Valid_NHS,9735019752,MURIEL,LOPEZ,female,2009-11-02,1 GREENFINCH DALE,GATEFORD,City,district,State,S81 8UL,UK,2014-05-23,2033-01-01 +Valid_NHS,9735023431,JULIAN,AQZ-MAR-PDS-ALPHA-ZERO,male,2005-04-30,18 BRAMBLE CLOSE,NORTH LEVERTON,City,district,State,DN22 0GD,UK,2011-03-12,2033-01-01 +Valid_NHS,9000187478,EDMOND,KLEIN,male,2006-03-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000197201,JAYME,FISHER,male,2000-08-04,Address Line 2 Address Line 1 Address Line 3,ADDRESS LINE 4,City,district,State,RG1 3BW,UK,2025-03-26,2033-01-01 +Valid_NHS,9000198410,ROBIN,QUINN,female,2009-03-26,Campus City New Campus 41 Berkeley Road Westbury Park,MANCHESTER,City,district,State,M16 0RA,UK,2025-03-26,2033-01-01 +Valid_NHS,9000209382,PORFIRIO,HOMENICK,male,2006-03-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9000213398,JOLIE,BOTSFORD,male,2006-03-26,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-26,2033-01-01 +Valid_NHS,9735053209,CATH,MCCOY,female,2003-07-11,35 MEXBOROUGH DRIVE,LEEDS,City,district,State,LS7 3EL,UK,2019-04-09,2033-01-01 +Valid_NHS,9735055597,MARK,HOWELL,male,2003-11-27,11 BLACKMOOR ROAD,LEEDS,City,district,State,LS17 5NB,UK,2013-12-23,2033-01-01 +Valid_NHS,9735055821,HANNAH,MOODIE,female,2005-12-31,103 SCOTT HALL ROAD,LEEDS,City,district,State,LS7 2HH,UK,2016-07-01,2033-01-01 +Valid_NHS,9735058839,JARROD,PRING,male,2007-04-18,11 ASKET HILL,LEEDS,City,district,State,LS8 2LY,UK,2021-07-17,2033-01-01 +Valid_NHS,9000161843,GEORGIA,DIER,male,2003-12-26,Sydenham House,Mill Ct,City,district,State,ZZ99 4BZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000162432,EUFEMIA,CREMIN,male,2006-03-25,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9000166683,ELLIS,OLIVIER,female,2009-05-28,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-25,2033-01-01 +Valid_NHS,9000182468,FAY,MANSELL,male,2001-06-04,Dartford 44 Rathbone Street Ms,MS,City,district,State,ZZ99 9GZ,UK,2025-03-25,2033-01-01 +Valid_NHS,9735017466,MATHEW,LLOYD,male,2009-05-11,1 SPEEDWELL PLACE,WORKSOP,City,district,State,S80 1UH,UK,2009-11-11,2033-01-01 +Valid_NHS,9735018179,CYRIL,ASTLEY,male,2007-11-17,ORCHARD HOUSE,CHURCH STREET,City,district,State,DN22 0BZ,UK,2012-08-08,2033-01-01 +Valid_NHS,9735019787,JULIAN,LAPPIN,male,2008-03-10,10 TOWN STREET,LOUND,City,district,State,DN22 8RS,UK,2015-12-29,2033-01-01 +Valid_NHS,9735023563,VERNON,AQZ-MAR-PDS-ALPHA-ZERO,male,2002-08-12,1 LIMES AVENUE,NETHER LANGWITH,City,district,State,NG20 9ET,UK,2021-05-05,2033-01-01 +Valid_NHS,9735025663,ALISON,AQZ-MAR-INVALID-PCFROM,female,2007-06-20,22 BLYTH ROAD,RANBY,City,district,State,DN22 8JH,UK,2013-06-03,2033-01-01 +Valid_NHS,9000069580,DON,FRIEDRICH,female,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2025-03-17,2033-01-01 +Valid_NHS,9000059879,MERTIE,LEXUS,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2025-03-14,2033-01-01 +Valid_NHS,5999552567,EARNEST,MEAGAN,female,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2025-02-10,2033-01-01 +Valid_NHS,9734599380,MINNIE,GEMMEL,female,2000-07-25,15 BIRCHWOOD AVENUE,LEEDS,City,district,State,LS17 8PJ,UK,2011-08-15,2033-01-01 +Valid_NHS,9734287028,JOSH,TODD,male,2000-07-25,1186 THE MOORS,KIDLINGTON,City,district,State,OX5 2AD,UK,2010-04-02,2033-01-01 +Valid_NHS,9734285122,AMIN,HAKHNAZARIAN,male,2000-07-25,1186 THE MOORS,KIDLINGTON,City,district,State,OX5 2AD,UK,2022-12-16,2033-01-01 +Valid_NHS,9734128779,GLENYS,PINOCK,female,2000-07-25,2 LOWCROFT,COLLINGHAM,City,district,State,LS22 5AQ,UK,2018-02-17,2033-01-01 +Valid_NHS,5998714504,NILS,MARYJANE,unknown,2000-07-25,24 London Street,LONDON,City,district,State,W2 1HH,UK,2024-11-05,2033-01-01 +Valid_NHS,9733433055,MINA,KITTS,female,2000-07-25,10 WOODFIELD ROAD,NOTTINGHAM,City,district,State,NG8 6JD,UK,2015-02-22,2033-01-01 +Valid_NHS,5997830896,AMERICA,FOREST,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-08-14,2033-01-01 +Valid_NHS,5997483436,VIVIAN,DESTINEE,male,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-07-18,2033-01-01 +Valid_NHS,5997291014,DELILAH,JONAS,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-06-28,2033-01-01 +Valid_NHS,5997297675,RYDER,FREDDY,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-06-28,2033-01-01 +Valid_NHS,5997140563,LAISHA,RIVER,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-06-18,2033-01-01 +Valid_NHS,5996947773,KEITH,SHAYNA,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-05-29,2033-01-01 +Valid_NHS,5996306557,REGINALD,JULIUS,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-02-28,2033-01-01 +Valid_NHS,5996246848,CHET,STACEY,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-02-23,2033-01-01 +Valid_NHS,5996165538,JEDIDIAH,ARACELY,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-02-19,2033-01-01 +Valid_NHS,5996064014,LURLINE,KEELEY,male,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-02-07,2033-01-01 +Valid_NHS,5996052113,AMYA,PAULA,male,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-02-07,2033-01-01 +Valid_NHS,5995901028,JARON,RUTHE,female,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-01-29,2033-01-01 +Valid_NHS,5995915754,LARON,ALISHA,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-01-29,2033-01-01 +Valid_NHS,5995894676,BARNEY,ERICKA,unknown,2000-07-25,Fulham Football Club Ltd Craven Cot,Stevenage Road,City,district,State,SW6 6HH,UK,2024-01-25,2033-01-01 +Valid_NHS,5995815229,ROSELYN,KENNITH,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-01-18,2033-01-01 +Valid_NHS,5995766171,MALLORY,ANDERSON,female,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-01-16,2033-01-01 +Valid_NHS,5995706098,VERNER,LELA,female,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-01-11,2033-01-01 +Valid_NHS,5995308556,SETH,LEANNE,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-11-18,2033-01-01 +Valid_NHS,5995223690,GONZALO,HERMAN,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2023-11-09,2033-01-01 +Valid_NHS,5995206958,LIBBIE,TERRENCE,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2023-11-09,2033-01-01 +Valid_NHS,5995163051,AMY,DANIELLE,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-11-08,2033-01-01 +Valid_NHS,9730088934,JOHN,ROWE,male,2000-07-25,10 NORTH STREET,ROXBY,City,district,State,DN15 0BL,UK,2001-08-11,2033-01-01 +Valid_NHS,5995022318,TERRELL,JEROMY,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-10-21,2033-01-01 +Valid_NHS,5994512680,RHETT,EWALD,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-09-08,2033-01-01 +Valid_NHS,5994434388,KAYLEE,AARON,male,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2023-09-05,2033-01-01 +Valid_NHS,5994375322,UNA,HAYLEY,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2023-09-01,2033-01-01 +Valid_NHS,5994266353,GAIL,HORACIO,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-08-28,2033-01-01 +Valid_NHS,5994220590,LIONEL,CHET,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-08-25,2033-01-01 +Valid_NHS,5994217395,EINAR,BRIELLE,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2023-08-24,2033-01-01 +Valid_NHS,5994167754,THAD,BRENDEN,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-08-22,2033-01-01 +Valid_NHS,5994099430,CLEMMIE,DEXTER,female,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2023-08-17,2033-01-01 +Valid_NHS,5994132357,ANNAMAE,VIDA,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-08-18,2033-01-01 +Valid_NHS,5990059019,JAKE,HILL,male,2000-07-25,31 Semele Close,Radford Semele,City,district,State,CV31 1UF,UK,2000-07-25,2033-01-01 +Valid_NHS,9473715021,IONA,MCKENZIE-COOK,unknown,2000-07-25,2 BRIDLE PATH WALK,LEEDS,City,district,State,LS15 7TN,UK,2016-02-08,2033-01-01 +Valid_NHS,9686928723,LILIAN,ASNIP,female,2000-07-25,3 MAIN STREET,SAXBY-ALL-SAINTS,City,district,State,DN20 0QJ,UK,2008-07-01,2033-01-01 +Valid_NHS,9687678232,LILLY,DEANE,female,2000-07-25,11 STATION ROAD,KEADBY,City,district,State,DN17 3BT,UK,2006-07-04,2033-01-01 +Valid_NHS,9471176190,JEWEL,DAUGHMA,unknown,2000-07-25,10 TAYLOR STREET,CLEETHORPES,City,district,State,DN35 7AX,UK,2016-02-08,2033-01-01 +Valid_NHS,9673488614,GAIL,LILLEY,female,2000-07-25,2 ABBEY RISE,BARROW-UPON-HUMBER,City,district,State,DN19 7TF,UK,2017-01-19,2033-01-01 +Valid_NHS,9691364204,BRIAN,MCJ-MAR-PDS-ALPHA-ZERO,male,2000-07-25,11 BELLINGHAM ROAD,SCUNTHORPE,City,district,State,DN16 1SB,UK,2018-02-13,2033-01-01 +Valid_NHS,9490339431,DOROTHEA,MALEK,female,2000-07-25,2 LEVEN COURT,BACKBARROW,City,district,State,LA12 8RB,UK,2002-07-16,2033-01-01 +Valid_NHS,9687080779,ALIS,POWELL-HUGHES,female,2000-07-25,1 DEWSBURY AVENUE,SCUNTHORPE,City,district,State,DN15 8AP,UK,2014-05-13,2033-01-01 +Valid_NHS,9460798799,ANORA,GRINBERG,unknown,2000-07-25,2 COLLEGE STREET,TODMORDEN,City,district,State,OL14 8LW,UK,2013-12-22,2033-01-01 +Valid_NHS,9687890819,LIZZY,LYONS,female,2000-07-25,22 ABBOTTS ROAD,SCUNTHORPE,City,district,State,DN17 1JG,UK,2017-09-03,2033-01-01 +Valid_NHS,9687895497,LOTTIE,BAKER,female,2000-07-25,1 POPPLEWELL TERRACE,EPWORTH,City,district,State,DN9 1HW,UK,2006-03-13,2033-01-01 +Valid_NHS,9690802151,MERVYN,FOSTER,male,2000-07-25,CRUACHAN,COMMONSIDE,City,district,State,DN17 4EY,UK,2008-04-25,2033-01-01 +Valid_NHS,9486535191,ELLISA,DUNN,female,2000-07-25,287 CROMWELL ROAD,PETERBOROUGH,City,district,State,PE1 2HP,UK,2016-04-21,2033-01-01 +Valid_NHS,9687494298,MARIA,BENYON,female,2000-07-25,2 PELHAM VILLAS,VICARAGE ROAD,City,district,State,DN20 8RS,UK,2005-03-02,2033-01-01 +Valid_NHS,9687120673,DAWN,TAFFE,female,2000-07-25,52 GROSVENOR STREET,SCUNTHORPE,City,district,State,DN15 6BB,UK,2010-11-28,2033-01-01 +Valid_NHS,9460806856,AKALSUKH,SUPPIAH,male,2000-07-25,1 NORGREAVE WAY,HALFWAY,City,district,State,S20 4TL,UK,2013-12-22,2033-01-01 +Valid_NHS,9687676515,PEGGY,HOLDEN,female,2000-07-25,1A,NORTH STREET,City,district,State,DN17 3JP,UK,2011-12-06,2033-01-01 +Valid_NHS,9472684009,TYREEK,DEVONALD,male,2000-07-25,BEETHAM HOUSE,BEETHAM,City,district,State,LA7 7AP,UK,2016-02-08,2033-01-01 +Valid_NHS,9687939591,LILIAN,DENSON,female,2000-07-25,102 RAVENDALE STREET SOUTH,SCUNTHORPE,City,district,State,DN15 6QG,UK,2005-05-09,2033-01-01 +Valid_NHS,9460902766,BRIANNA,THACKRAY,female,2000-07-25,22 FAIR VIEW,WEST RAINTON,City,district,State,DH4 6RX,UK,2013-12-22,2033-01-01 +Valid_NHS,9687690666,BESSIE,HOBSON,female,2000-07-25,1 BARROW ROAD,NEW HOLLAND,City,district,State,DN19 7QG,UK,2010-08-28,2033-01-01 +Valid_NHS,9473196571,TAMIKA,HUTCHINGS,female,2000-07-25,10 WILTON CLOSE,RAWMARSH,City,district,State,S62 6ND,UK,2016-02-08,2033-01-01 +Valid_NHS,9490174874,ZARINE,JOBANPUTRA,female,2000-07-25,1 COOLIDGE AVENUE,LANCASTER,City,district,State,LA1 5EG,UK,2013-06-17,2033-01-01 +Valid_NHS,9687985089,SIAN,BREEZE,female,2000-07-25,102 NEWLAND DRIVE,SCUNTHORPE,City,district,State,DN15 7HW,UK,2016-10-29,2033-01-01 +Valid_NHS,9473102321,DARIA,MARLAND,female,2000-07-25,1 VERE ROAD,SHEFFIELD,City,district,State,S6 1SA,UK,2016-02-08,2033-01-01 +Valid_NHS,9687198060,MAISIE,BANKS,female,2000-07-25,ALLANDALE,FERNERIES LANE,City,district,State,DN38 6HN,UK,2002-03-30,2033-01-01 +Valid_NHS,9687667427,BLANCH,COLMAN,female,2000-07-25,2 OLD MANOR DRIVE,SCAWBY,City,district,State,DN20 9FJ,UK,2015-09-19,2033-01-01 +Valid_NHS,9464337168,ENID,MALLON,female,2000-07-25,101 MANOR FARM DRIVE,LEEDS,City,district,State,LS10 3RQ,UK,2012-09-21,2033-01-01 +Valid_NHS,9461391307,RODDY,ALEXANDER-SMITH,male,2000-07-25,74 LUMLEY STREET,CASTLEFORD,City,district,State,WF10 5LD,UK,2013-12-22,2033-01-01 +Valid_NHS,9471018927,LETHA,CLENCH,unknown,2000-07-25,1 BURNTHOUSE CLOSE,BLAYDON-ON-TYNE,City,district,State,NE21 6ET,UK,2016-02-08,2033-01-01 +Valid_NHS,9473077955,LANA,NORTHFIELD,female,2000-07-25,12 OAKWAY,BIRKENSHAW,City,district,State,BD11 2PG,UK,2016-02-08,2033-01-01 +Valid_NHS,9473120117,DOTTY,NETO,female,2000-07-25,7 BRONTE RISE,CASTLEFORD,City,district,State,WF10 3TU,UK,2016-02-08,2033-01-01 +Valid_NHS,9687757914,ZHU,NI,female,2000-07-25,19 PETERBOROUGH ROAD,SCUNTHORPE,City,district,State,DN16 2DS,UK,2016-11-28,2033-01-01 +Valid_NHS,9692958450,WARREN,TOBIN,male,2000-07-25,10 CASTLE HILL,BECCLES,City,district,State,NR34 7BH,UK,2018-11-24,2033-01-01 +Valid_NHS,9461167830,TEALE,KERNAN,female,2000-07-25,10 LILAC TERRACE,SHOTTON COLLIERY,City,district,State,DH6 2HS,UK,2013-12-22,2033-01-01 +Valid_NHS,9464406070,DONALDA,HOLDWAY,female,2000-07-25,1 ROSEDALE AVENUE,HAYES,City,district,State,UB3 2RG,UK,2014-11-11,2033-01-01 +Valid_NHS,9725835344,PENNY,LESLIE,female,2000-07-25,25 BARNOLDBY ROAD,WALTHAM,City,district,State,DN37 0BS,UK,2010-01-20,2033-01-01 +Valid_NHS,9473103786,ANONA,MAGNUS,female,2000-07-25,11 BLYTH AVENUE,RAWMARSH,City,district,State,S62 7AS,UK,2016-02-08,2033-01-01 +Valid_NHS,9473152728,EVENEZER,ABRAHAM,male,2000-07-25,11 WOODSIDE,HARROGATE,City,district,State,HG1 5NG,UK,2016-02-08,2033-01-01 +Valid_NHS,9652976679,SAMUEL,WOOSEY,male,2000-07-25,38 WEELSBY ROAD,GRIMSBY,City,district,State,DN32 0PR,UK,2008-01-11,2033-01-01 +Valid_NHS,9687498250,SANGAT,CHAHALL,female,2000-07-25,1 DARBY ROAD,BURTON-UPON-STATHER,City,district,State,DN15 9DY,UK,2013-12-15,2033-01-01 +Valid_NHS,9464005696,PENG,HO,male,2000-07-25,1 CITY LANE,HALIFAX,City,district,State,HX3 5LE,UK,2007-07-16,2033-01-01 +Valid_NHS,9686690565,OLIVE,CARDEN,female,2000-07-25,1 VALENTINE CLOSE,HULL,City,district,State,HU4 7DN,UK,2014-04-24,2033-01-01 +Valid_NHS,5993284544,GLADYS,LEANNON,female,2000-07-25,174 Bernhard Stream Gutkowskiview,GB,City,district,State,CR9 2BY,UK,2023-04-27,2033-01-01 +Valid_NHS,9728528272,NETTA,HAYDEN,female,2000-07-25,PARK LODGE,SCAWBY BROOK,City,district,State,DN20 9LD,UK,2004-06-17,2033-01-01 +Valid_NHS,5994894796,PIERCE,CARLIE,male,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-10-13,2033-01-01 +Valid_NHS,5994913413,KAVON,AUBREE,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-10-14,2033-01-01 +Valid_NHS,5995039792,VICKY,ILA,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2023-10-23,2033-01-01 +Valid_NHS,5995110349,HILBERT,JAIDEN,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2023-11-06,2033-01-01 +Valid_NHS,5995132407,GREEN,ORAL,unknown,2000-07-25,Fulham Football Club Ltd Craven Cot,Stevenage Road,City,district,State,SW6 6HH,UK,2023-11-06,2033-01-01 +Valid_NHS,5996103028,ELVA,KEAGAN,male,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-02-12,2033-01-01 +Valid_NHS,9731553495,MATT,CURRIE,male,2000-07-25,1 Herries Road,Sheffield,City,district,State,S5 7AU,UK,2009-04-04,2033-01-01 +Valid_NHS,5996425289,AUBREY,JUANA,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-03-15,2033-01-01 +Valid_NHS,5996471663,ERIK,MARIAM,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-03-16,2033-01-01 +Valid_NHS,5996676834,VERLA,ELEONORE,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-04-09,2033-01-01 +Valid_NHS,5996790041,VLADIMIR,CAMRON,unknown,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-04-23,2033-01-01 +Valid_NHS,5996793482,JACLYN,JACYNTHE,unknown,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-04-23,2033-01-01 +Valid_NHS,5996846853,DONALD,ELTA,male,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-05-18,2033-01-01 +Valid_NHS,5996849763,SALVATORE,MOSE,female,2000-07-25,11 Stevenson Square,MANCHESTER,City,district,State,M1 1DB,UK,2024-05-18,2033-01-01 +Valid_NHS,9732390808,PEGGY,PENSON,female,2000-07-25,1 AVENUE FONTENAY,SCUNTHORPE,City,district,State,DN15 8EN,UK,2023-05-11,2033-01-01 +Valid_NHS,5998095499,SANTOS,WOLF,male,2000-07-25,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,M16 0RA,UK,2024-09-04,2033-01-01 +Valid_NHS,9733448451,MILLIE,HASNIP,female,2000-07-25,1 Herries Road,Sheffield,City,district,State,S5 7AU,UK,2012-12-01,2033-01-01 +Valid_NHS,5998725050,DOMENIC,VERNON,male,2000-07-25,1 New Road,SOLIHULL,City,district,State,B91 3DL,UK,2024-11-06,2033-01-01 +Valid_NHS,9000014700,CLAUDE,NIELSEN,female,2002-03-13,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-13,2033-01-01 +Valid_NHS,9000000513,ROBIN,OWEN,female,2002-03-13,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-13,2033-01-01 +Valid_NHS,9734367684,CERI,TUSON,female,2002-03-13,INGS LODGE,LINTON ROAD,City,district,State,LS22 6HD,UK,2017-02-02,2033-01-01 +Valid_NHS,9733915017,DAVINA,WESTON,female,2002-03-13,49 WALTON HALL AVENUE,LIVERPOOL,City,district,State,L4 6TB,UK,2021-04-13,2033-01-01 +Valid_NHS,9733767980,ALAN,COOMBS,male,2002-03-13,2 Crouch Ave,Barking,City,district,State,IG11 0QZ,UK,2003-01-19,2033-01-01 +Valid_NHS,5996637758,DAMION,BAHRINGER,male,2002-03-13,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-04-03,2033-01-01 +Valid_NHS,9730614687,DIANE,KIRKUS,female,2002-03-13,LITTLE GRANGE,FERRIBY ROAD,City,district,State,DN18 5RJ,UK,2010-01-28,2033-01-01 +Valid_NHS,9728858442,JESSIE,SWAIN,female,2002-03-13,47 FIRST AVENUE,LONDON,City,district,State,W3 7JN,UK,2017-10-24,2033-01-01 +Valid_NHS,5993070021,CARLOS,MAYERT,male,2002-03-13,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,ZZ99 6CZ,UK,2023-03-21,2033-01-01 +Valid_NHS,9486159661,CELIA,WILSON,female,2002-03-13,19 THIRLMERE ROAD,LANCASTER,City,district,State,LA1 3LJ,UK,2013-09-04,2033-01-01 +Valid_NHS,9486224099,ABDUL-NASIR,AL-YAMANI,male,2002-03-13,1 HART STREET,ULVERSTON,City,district,State,LA12 7HY,UK,2005-06-10,2033-01-01 +Valid_NHS,5991215928,VASILIKI,KREIGER,male,2002-03-13,CAMPUS CITY NEW CAMPUS 41 BERKELEY ROAD,WESTBURY PARK,City,district,State,ZZ99 4GZ,UK,2022-03-08,2033-01-01 +Valid_NHS,9485559917,IVOR,NOLAN,male,2002-03-13,39 LIGHTBURN ROAD,ULVERSTON,City,district,State,LA12 0BX,UK,2009-07-21,2033-01-01 +Valid_NHS,9471009626,CHERI,STAMBOULAKIS,female,2002-03-13,1 ST. ANDREWS WALK,LEEDS,City,district,State,LS17 7TS,UK,2016-02-08,2033-01-01 +Valid_NHS,9650877223,SANDRA,FERNS,female,2002-03-13,10 REVESBY AVENUE,GRIMSBY,City,district,State,DN34 5JW,UK,2015-06-25,2033-01-01 +Valid_NHS,9652744832,HOWARD,FAYLE,male,2002-03-13,10 CRANEMORE,PETERBOROUGH,City,district,State,PE4 5AJ,UK,2011-12-30,2033-01-01 +Valid_NHS,9464560053,RITA,SYKES,female,2002-03-13,1 PRINCES ROAD WEST,TORQUAY,City,district,State,TQ1 1PB,UK,2012-09-21,2033-01-01 +Valid_NHS,9651678720,EDMUND,STOTT,male,2002-03-13,1 BERMONDSEY DRIVE,HULL,City,district,State,HU5 5EH,UK,2007-03-28,2033-01-01 +Valid_NHS,9657962137,MAE,GOSS,unknown,2002-03-13,10 BAMBOROUGH TERRACE,NORTH SHIELDS,City,district,State,NE30 2BU,UK,2003-02-16,2033-01-01 +Valid_NHS,9459995535,NORMAND,SONGU,unknown,2002-03-13,1 WALKER CLOSE,GLUSBURN,City,district,State,BD20 8PW,UK,2013-12-22,2033-01-01 +Valid_NHS,9485404263,MIKALA,WOOSEY,female,2002-03-13,1 BROOKFIELD TERRACE,BAY HORSE,City,district,State,LA2 9AG,UK,2008-01-04,2033-01-01 +Valid_NHS,9693137175,SIBYL,MCADAM,female,2002-03-13,MYHOLME,COLLEGE ROAD,City,district,State,DN40 3PN,UK,2012-12-13,2033-01-01 +Valid_NHS,9481842592,IAN,CATON,male,2002-03-13,1 FAWCETT CLOSE,HULL,City,district,State,HU3 1TG,UK,2007-10-30,2033-01-01 +Valid_NHS,9464546239,JANNAH,KEOHANE,unknown,2002-03-13,1 BOLEYN CLOSE,GRANGE PARK,City,district,State,SN5 6JZ,UK,2007-07-19,2033-01-01 +Valid_NHS,9461207875,BETHANIE,WINDEATT,female,2002-03-13,1 THE AVENUE,CAMPSALL,City,district,State,DN6 9NB,UK,2013-12-22,2033-01-01 +Valid_NHS,9651673125,BELLE,CARSON,female,2002-03-13,UNIT 1,GOULTON STREET,City,district,State,HU3 4DD,UK,2015-02-07,2033-01-01 +Valid_NHS,9694189802,MARIE,HEYES,unknown,2002-03-13,13 NEWBY STREET,LONDON,City,district,State,SW8 3BQ,UK,2014-11-12,2033-01-01 +Valid_NHS,9481845699,CHAS,ECKES,male,2002-03-13,1 CALTHORPE STREET,HULL,City,district,State,HU3 3HU,UK,2016-05-26,2033-01-01 +Valid_NHS,9651865466,NADIA,COOK,unknown,2002-03-13,113 BRISTOL ROAD,HULL,City,district,State,HU5 5XW,UK,2009-11-12,2033-01-01 +Valid_NHS,9726774098,MOSES,IQZ-APR-PDS-ALPHA-ZERO,male,2002-03-13,BUTTERSCROFT,COLLEGE ROAD,City,district,State,DN19 7LP,UK,2005-09-04,2033-01-01 +Valid_NHS,9462706379,SHARON,SEFTON,female,2002-03-13,11 BANKS RISE,BENTHAM,City,district,State,LA2 7JW,UK,2012-09-21,2033-01-01 +Valid_NHS,5991463751,CLYDE,WIZA,male,2002-03-13,CAMPUS CITY NEW CAMPUS 41 BERKELEY ROAD,WESTBURY PARK,City,district,State,ZZ99 4FZ,UK,2022-05-17,2033-01-01 +Valid_NHS,9460883664,CASSIAN,BAIG,male,2002-03-13,3 REDWOOD WAY,BRIDLINGTON,City,district,State,YO16 7GY,UK,2013-12-22,2033-01-01 +Valid_NHS,9650372733,SOPHIE,SAMUEL,female,2002-03-13,27 OWEN ROAD,LANCASTER,City,district,State,LA1 2LL,UK,2009-11-20,2033-01-01 +Valid_NHS,9658111068,TREVOR,COWEN,male,2002-03-13,1 PINE TREE AVENUE,BOSTON SPA,City,district,State,LS23 6HA,UK,2016-02-16,2033-01-01 +Valid_NHS,9466448366,BEULAH,GODAZ,female,2002-03-13,11 UNITY STREET,CARLTON,City,district,State,WF3 3RA,UK,2015-04-23,2033-01-01 +Valid_NHS,9473072236,BOOKER,WYKE-SMITH,unknown,2002-03-13,11 PRIMLEY PARK DRIVE,LEEDS,City,district,State,LS17 7LP,UK,2016-02-08,2033-01-01 +Valid_NHS,9466583484,JEFFERSON,NYAMU,male,2002-03-13,110 GREAT COATES ROAD,GREAT COATES,City,district,State,DN37 9NS,UK,2015-09-02,2033-01-01 +Valid_NHS,9650903135,IAN,MOLLOY,male,2002-03-13,14A,OSBORNE STREET,City,district,State,DN35 8LB,UK,2004-08-29,2033-01-01 +Valid_NHS,9657901979,ALLYN,ROLLESTON,male,2002-03-13,200 HOPE CARR ROAD,LEIGH,City,district,State,WN7 3AL,UK,2006-04-29,2033-01-01 +Valid_NHS,9650319204,GILIT,BEN-ZERT,female,2002-03-13,1 GOWAN CRESCENT,STAVELEY,City,district,State,LA8 9NF,UK,2002-12-09,2033-01-01 +Valid_NHS,9461195745,MINDA,RISHI,female,2002-03-13,1 SCHOFIELD CLOSE,BARROW-UPON-HUMBER,City,district,State,DN19 7SJ,UK,2013-12-22,2033-01-01 +Valid_NHS,9689183621,GRETA,KENNY,female,2002-03-13,34 KIRKHALL LANE,LEIGH,City,district,State,WN7 1SA,UK,2013-05-11,2033-01-01 +Valid_NHS,9729868832,DUNCAN,LJZ-SEP-PDS-ALPHA-ZERO,male,2002-03-13,1 BIRCH CLOSE,NUTHALL,City,district,State,NG16 1FQ,UK,2010-09-20,2033-01-01 +Valid_NHS,5994779806,GEORGEANNA,LARSON,female,2002-03-13,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-10-04,2033-01-01 +Valid_NHS,5995351028,PARIS,YUNDT,male,2002-03-13,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-20,2033-01-01 +Valid_NHS,9732376465,ELSIE,VALE,female,2002-03-13,PRINCESS HOUSE,SCUNTHORPE,City,district,State,DN15 6SJ,UK,2010-02-18,2033-01-01 +Valid_NHS,9732951176,BETH,HAVEY,female,2002-03-13,29 PHEASANTS WAY,RICKMANSWORTH,City,district,State,WD3 7EX,UK,2018-02-15,2033-01-01 +Valid_NHS,5999754755,ASHLYN,KOSS,male,2002-03-13,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 3AZ,UK,2025-02-21,2033-01-01 +Valid_NHS,9733903000,LEO,GCZ-NOV-PDS-ALPHA-ZERO,male,2003-02-05,1 WELTON CLOSE,BARTON-UPON-HUMBER,City,district,State,DN18 5PB,UK,2012-03-24,2033-01-01 +Valid_NHS,9732162589,SEETAL,GOPALAKRISHNAN,female,2003-02-05,400 Hampstel Rd,Harlow,City,district,State,CM20 1QX,UK,2021-12-09,2033-01-01 +Valid_NHS,5996034549,ELVIN,JACKSON,female,2003-02-05,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-02-05,2033-01-01 +Valid_NHS,5994856290,COURTNEY,MILLER,male,2003-02-05,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,ZZ99 4GZ,UK,2023-10-12,2033-01-01 +Valid_NHS,9729921652,TRACEY,ROTDEN,female,2003-02-05,GREY FRIARS,CHURCH VIEW,City,district,State,DN15 9JE,UK,2004-06-03,2033-01-01 +Valid_NHS,9728397038,CALVIN,JOHNS,male,2003-02-05,WOODSIDE,CURTIS CLOSE,City,district,State,DN18 6LE,UK,2022-06-06,2033-01-01 +Valid_NHS,9460804829,MERV,DENCOURT,male,2003-02-05,16 TOWNHEAD ROAD,SHEFFIELD,City,district,State,S17 3GA,UK,2013-12-22,2033-01-01 +Valid_NHS,9461504861,ASHLYNN,KALIA,female,2003-02-05,1 SYCAMORE DRIVE,HALIFAX,City,district,State,HX3 8XD,UK,2013-12-22,2033-01-01 +Valid_NHS,9490124648,ELSIE,PRCE,female,2003-02-05,101 BLEASWOOD ROAD,OXENHOLME,City,district,State,LA9 7EW,UK,2010-06-19,2033-01-01 +Valid_NHS,9658133010,FENGGE,TSEUN,male,2003-02-05,10 EGGLESTONE SQUARE,BOSTON SPA,City,district,State,LS23 6RX,UK,2009-07-12,2033-01-01 +Valid_NHS,9459950892,PEARCE,BEDDIS,unknown,2003-02-05,72 GOATHLAND AVENUE,NEWCASTLE UPON TYNE,City,district,State,NE12 8HE,UK,2013-12-22,2033-01-01 +Valid_NHS,9485302118,ELLISA,CHEW,unknown,2003-02-05,2 BROOKSIDE DRIVE,DOLPHINHOLME,City,district,State,LA2 9AZ,UK,2007-06-24,2033-01-01 +Valid_NHS,9468052761,LUCAS,MCRAE,male,2003-02-05,11 HILLSIDE TERRACE,BRADFORD,City,district,State,BD3 0BD,UK,2015-08-05,2033-01-01 +Valid_NHS,9692906213,TREVOR,LOCKER,male,2003-02-05,1 BIRCHAM CRESCENT,KIRTON LINDSEY,City,district,State,DN21 4PT,UK,2018-03-12,2033-01-01 +Valid_NHS,9725634063,RAMONA,BILBY,female,2003-02-05,10 HERON CLOSE,GRIMSBY,City,district,State,DN32 8PW,UK,2019-08-05,2033-01-01 +Valid_NHS,9485497997,AUDREY,NEWALL,unknown,2003-02-05,12 PRIORY ROAD,ULVERSTON,City,district,State,LA12 9HS,UK,2014-10-23,2033-01-01 +Valid_NHS,9657711916,LANCE,ALLEN,male,2003-02-05,11 CAMBRIDGE AVENUE,PETERBOROUGH,City,district,State,PE1 2JA,UK,2017-12-20,2033-01-01 +Valid_NHS,9651730447,TESSA,GILLI,unknown,2003-02-05,728 HOTHAM ROAD SOUTH,HULL,City,district,State,HU5 5LF,UK,2017-02-01,2033-01-01 +Valid_NHS,9460976751,MARISSA,PILGRIM-CARVER,female,2003-02-05,2 HOLLY BANK COURT,HALIFAX,City,district,State,HX3 8PE,UK,2013-12-22,2033-01-01 +Valid_NHS,9473136021,SHERLEY,HOWLAND,female,2003-02-05,12 SOUTH VIEW,BRAFFERTON,City,district,State,DL1 3LB,UK,2016-02-08,2033-01-01 +Valid_NHS,9650142975,IRVING,MURTA,male,2003-02-05,1 HALL GARTH,BARROW-IN-FURNESS,City,district,State,LA13 0QT,UK,2009-12-08,2033-01-01 +Valid_NHS,9650411801,KATIE,SCARFF,female,2003-02-05,10 RUSKIN DRIVE,KIRKBY LONSDALE,City,district,State,LA6 2DB,UK,2004-08-09,2033-01-01 +Valid_NHS,9485341210,EUNICE,COYNE,female,2003-02-05,105 BUCCLEUCH STREET,BARROW-IN-FURNESS,City,district,State,LA14 1AW,UK,2012-07-13,2033-01-01 +Valid_NHS,9460932703,SALINA,GACHETTE,female,2003-02-05,1 MANOR FARM COTTAGE,ELSHAM WOLDS,City,district,State,DN20 0NU,UK,2013-12-22,2033-01-01 +Valid_NHS,9468049027,BRAD,DAWBER,male,2003-02-05,10 ST. MARGARETS TERRACE,BRADFORD,City,district,State,BD7 3AP,UK,2010-03-05,2033-01-01 +Valid_NHS,9475693149,FAYE,CLEARY,unknown,2003-02-05,null,null,City,district,State,BD7 3AP,UK,2010-03-05,2033-01-01 +Valid_NHS,9694314526,NINA,HYCOCK,female,2003-02-05,10 KETTLEBY VIEW,BRIGG,City,district,State,DN20 8UD,UK,2006-12-22,2033-01-01 +Valid_NHS,9460780814,TANNER,HEALEY,unknown,2003-02-05,10 WESTMORLAND AVENUE,NEWBIGGIN-BY-THE-SEA,City,district,State,NE64 6RW,UK,2013-12-22,2033-01-01 +Valid_NHS,9490155551,NIGEL,TOOTLE,male,2003-02-05,10 CAVENDISH STREET,DALTON-IN-FURNESS,City,district,State,LA15 8SP,UK,2008-11-29,2033-01-01 +Valid_NHS,9674890327,ARLENE,PECK,female,2003-02-05,26 LUXOR ROAD,LEEDS,City,district,State,LS8 5BJ,UK,2017-02-20,2033-01-01 +Valid_NHS,9692625346,FAYE,WHALON,unknown,2003-02-05,10 SUNNINGDALE AVENUE,BRIGG,City,district,State,DN20 8QD,UK,2008-08-17,2033-01-01 +Valid_NHS,9461504756,JANHAVI,RESHAM,female,2003-02-05,2 HOLLY BANK COURT,HALIFAX,City,district,State,HX3 8PE,UK,2013-12-22,2033-01-01 +Valid_NHS,9694186978,IRVING,ALCOCK,male,2003-02-05,10 STREATHAM HILL,LONDON,City,district,State,SW2 4AE,UK,2015-01-14,2033-01-01 +Valid_NHS,9727310095,VICKI,DISLEY,unknown,2003-02-05,10 ANDERSON ROAD,SCUNTHORPE,City,district,State,DN16 1PU,UK,2003-10-10,2033-01-01 +Valid_NHS,9651198761,ARCHIE,READY,male,2003-02-05,17 NORTH STREET,WEST BUTTERWICK,City,district,State,DN17 3JR,UK,2003-08-01,2033-01-01 +Valid_NHS,9693536967,JUDY,HOMAN,female,2003-02-05,ORPHANS YARD,BRIXTON STATION ROAD,City,district,State,SW9 8QB,UK,2009-06-28,2033-01-01 +Valid_NHS,9457827163,EMMALINE,KAOMA,unknown,2003-02-05,41 GREENTOP,PUDSEY,City,district,State,LS28 8JB,UK,2013-12-22,2033-01-01 +Valid_NHS,9485327366,GARY,PEGLAR,male,2003-02-05,HIGHFIELD LODGE,QUERNMORE ROAD,City,district,State,LA1 3JT,UK,2010-03-04,2033-01-01 +Valid_NHS,9485565291,KURT,BOWERS,male,2003-02-05,50 EWAN CLOSE,BARROW-IN-FURNESS,City,district,State,LA13 9UL,UK,2006-10-10,2033-01-01 +Valid_NHS,9461091923,ELRIC,BEVENS,male,2003-02-05,1 LIMES AVENUE,GAWBER,City,district,State,S75 2JA,UK,2013-12-22,2033-01-01 +Valid_NHS,9651635207,JOEL,IRWIN,male,2003-02-05,1 WOLD ROAD,HULL,City,district,State,HU5 5LT,UK,2011-02-09,2033-01-01 +Valid_NHS,5990109296,HARRY,KANE,male,2003-02-05,Evolve Hairdressing 123A,Vicar Lane,City,district,State,LS1 6PJ,UK,2019-10-24,2033-01-01 +Valid_NHS,5993249358,TOMMY,PADBERG,female,2003-02-05,463,LONDON,City,district,State,ZY2R 9HG,UK,2023-04-20,2033-01-01 +Valid_NHS,5993911956,DARREL,HANE,male,2003-02-05,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-08-03,2033-01-01 +Valid_NHS,9730438226,WAYNE,GAFNEY,male,2003-02-05,1B,ST. PAULS ROAD,City,district,State,DN16 3DL,UK,2007-01-26,2033-01-01 +Valid_NHS,5995521772,CRISTA,HYATT,male,2003-02-05,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,ZZ99 6CZ,UK,2023-12-11,2033-01-01 +Valid_NHS,5995853503,EVALYN,GUSIKOWSKI,male,2003-02-05,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2024-01-19,2033-01-01 +Valid_NHS,9730838887,JARROD,YHZ-FEB-INVALIDTEL6,male,2003-02-05,189 SCOTTER ROAD,SCUNTHORPE,City,district,State,DN15 8AZ,UK,2007-09-11,2033-01-01 +Valid_NHS,9732868031,DAVINA,PUELMA,female,2003-02-05,SHAFTSBURY COURT,BARNSTAPLE ROAD,City,district,State,DN17 1YB,UK,2024-04-14,2033-01-01 +Valid_NHS,5999178223,MICHAEL,HOUSEMAN,male,2003-02-05,Ghansi Bazaar,Hyderabad,City,district,State,ZZ99 6CZ,UK,2025-01-14,2033-01-01 +Valid_NHS,9734735179,KENT,HAYLE,male,2003-02-05,Praed Street,London,City,district,State,W2 1NY,UK,2008-10-25,2033-01-01 +Valid_NHS,9734269224,BASIL,LOCK,male,2003-10-24,1186 THE MOORS,KIDLINGTON,City,district,State,OX5 2AD,UK,2014-05-13,2033-01-01 +Valid_NHS,9734335588,ETTIE,QUIRK,female,2003-10-24,1186 THE MOORS,KIDLINGTON,City,district,State,OX4 4BU,UK,2007-06-12,2033-01-01 +Valid_NHS,5996916835,DEXTER,ARMSTRONG,male,2003-10-24,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-05-23,2033-01-01 +Valid_NHS,9730146713,MARTHA,NICOL,unknown,2003-10-24,11 LABURNUM AVENUE,WALLSEND,City,district,State,NE28 8HQ,UK,2021-05-23,2033-01-01 +Valid_NHS,5995065645,JEWELL,HACKETT,male,2003-10-24,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-10-24,2033-01-01 +Valid_NHS,9729110891,STELLA,OLDHAM,female,2003-10-24,10 CALDWELL STREET,LONDON,City,district,State,SW9 0EQ,UK,2015-01-14,2033-01-01 +Valid_NHS,5993845759,HERMINA,THOMPSON,female,2003-10-24,79634 Wyman Glens Lakeedmundotown,GB,City,district,State,CR9 2BY,UK,2023-07-28,2033-01-01 +Valid_NHS,9481384608,CLIVE,CANNON,male,2003-10-24,101 HIGH STREET,BROUGHTON,City,district,State,DN20 0JR,UK,2016-04-28,2033-01-01 +Valid_NHS,9460085865,MICHEAL,OLDS,male,2003-10-24,11 GLADSTONE ROAD,SCARBOROUGH,City,district,State,YO12 7BQ,UK,2013-12-22,2033-01-01 +Valid_NHS,9658056547,ASHLEY,MERCER,female,2003-10-24,10 WOODLIFFE COURT,LEEDS,City,district,State,LS7 3RF,UK,2013-01-04,2033-01-01 +Valid_NHS,9651158301,AMANDA,GILLI,female,2003-10-24,1 CHURCHILL AVENUE,LEOMINSTER,City,district,State,HR6 8HY,UK,2010-05-25,2033-01-01 +Valid_NHS,9459969682,DELILAH,SCHEYBAL,unknown,2003-10-24,1 BERKELEY STREET,SOUTH SHIELDS,City,district,State,NE33 2SX,UK,2013-12-22,2033-01-01 +Valid_NHS,9460776930,BERNETTA,ROOTES,unknown,2003-10-24,10 WINDHILL ROAD,NEWCASTLE UPON TYNE,City,district,State,NE6 3TQ,UK,2013-12-22,2033-01-01 +Valid_NHS,5992267565,BEN,MURRAY,male,2003-10-24,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4QZ,UK,2022-10-24,2033-01-01 +Valid_NHS,9482916425,CLARE,LUPTON,female,2003-10-24,GRANGE COTTAGE,BRIGG ROAD,City,district,State,LN7 6LF,UK,2007-06-11,2033-01-01 +Valid_NHS,9652636355,DAREN,TEMPLE,male,2003-10-24,1 CASTLE VIEW,AMBLE,City,district,State,NE65 0NL,UK,2016-08-16,2033-01-01 +Valid_NHS,9691715910,ADAM,HENRY,male,2003-10-24,32 BRETHERGATE,WESTWOODSIDE,City,district,State,DN9 2AD,UK,2008-09-20,2033-01-01 +Valid_NHS,5992262768,JASPER,DUBUQUE,female,2003-10-24,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-10-24,2033-01-01 +Valid_NHS,9651257369,LIVTAR,MORE,male,2003-10-24,2 OUTRAM MEWS,UPPERMILL,City,district,State,OL3 6BE,UK,2013-04-16,2033-01-01 +Valid_NHS,9457824350,UTTAMVEER,LALLY,unknown,2003-10-24,126A,WAKEFIELD ROAD,City,district,State,LS26 0SB,UK,2013-12-22,2033-01-01 +Valid_NHS,9690748599,DIANE,BRUCE,female,2003-10-24,10 HINDON WALK,SCUNTHORPE,City,district,State,DN17 1UF,UK,2014-08-01,2033-01-01 +Valid_NHS,9694337577,ELI,FINN,male,2003-10-24,21 CABBELL ROAD,CROMER,City,district,State,NR27 9HU,UK,2007-01-27,2033-01-01 +Valid_NHS,5992277773,NELL,JERRY,female,2003-10-24,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-10-24,2033-01-01 +Valid_NHS,9470027140,AMBROSE,GUMMER,male,2003-10-24,1 EDEN GROVE,BOLTON LE SANDS,City,district,State,LA5 8DF,UK,2016-02-08,2033-01-01 +Valid_NHS,9490275417,JAMES,REES,male,2003-10-24,BAXTON HOLME,GARNETT BRIDGE ROAD,City,district,State,LA8 9AU,UK,2006-06-04,2033-01-01 +Valid_NHS,9726753538,CUIFEN,SAM,female,2003-10-24,11 GARRADS ROAD,LONDON,City,district,State,SW16 1JU,UK,2020-05-09,2033-01-01 +Valid_NHS,9470150724,ROMAYNE,SEYS,female,2003-10-24,1 LINCOLN ROAD,CLEETHORPES,City,district,State,DN35 9LY,UK,2016-02-08,2033-01-01 +Valid_NHS,9651896337,TONY,STUART,male,2003-10-24,1 HOUGHTON WALK,HULL,City,district,State,HU5 5QZ,UK,2009-01-08,2033-01-01 +Valid_NHS,9459953654,LUCASTA,MEHMET-EMIN,unknown,2003-10-24,81 KINGSWAY,SUNNISIDE,City,district,State,NE16 5PH,UK,2013-12-22,2033-01-01 +Valid_NHS,9467288680,HOMER,HIPKISS,male,2003-10-24,278 HILLMORTON ROAD,RUGBY,City,district,State,CV22 5BW,UK,2015-10-27,2033-01-01 +Valid_NHS,9692647412,MURIEL,SEARLE,unknown,2003-10-24,2 GELDER BECK ROAD,MESSINGHAM,City,district,State,DN17 3UJ,UK,2017-01-17,2033-01-01 +Valid_NHS,9692958159,STEVEN,MCEVOY,male,2003-10-24,10 GLENWOOD DRIVE,WORLINGHAM,City,district,State,NR34 7DR,UK,2007-11-26,2033-01-01 +Valid_NHS,5991043655,NELSON,ZULAUF,male,2003-10-24,CAMPUS CITY NEW CAMPUS 41 BERKELEY ROAD,WESTBURY PARK,City,district,State,ZZ99 4FZ,UK,2022-01-31,2033-01-01 +Valid_NHS,5992265457,ADOLFO,WILDERMAN,male,2003-10-24,Bad Oyenhausen,Schulstrasse,City,district,State,ZZ99 4QZ,UK,2022-10-24,2033-01-01 +Valid_NHS,9469748387,CRISTAL,HEARN,unknown,2003-10-24,16 JENKIN RISE,KENDAL,City,district,State,LA9 6JN,UK,2016-02-08,2033-01-01 +Valid_NHS,9458068452,DIXIE,VELACRITCHLOW,female,2003-10-24,EAST HADLOW,ALBERT PROMENADE,City,district,State,HX3 0HZ,UK,2013-12-22,2033-01-01 +Valid_NHS,9471112710,CHEYANNE,OSWICK,female,2003-10-24,1 MIDWAY AVENUE,BINGLEY,City,district,State,BD16 1RN,UK,2016-02-08,2033-01-01 +Valid_NHS,9485414250,YVETTE,HAY,female,2003-10-24,ROSE COTTAGE,GARSDALE,City,district,State,LA10 5PQ,UK,2009-09-23,2033-01-01 +Valid_NHS,9693387872,STELLA,MASSAM,female,2003-10-24,170A,CLIVE ROAD,City,district,State,SE21 8BS,UK,2009-04-24,2033-01-01 +Valid_NHS,9486369526,BRIANA,WITTERING,female,2003-10-24,2 LECKBARROW COTTAGES,BROW EDGE ROAD,City,district,State,LA12 8PP,UK,2012-05-15,2033-01-01 +Valid_NHS,9650886818,JAYANI,SUCHI,female,2003-10-24,2 QUEENS ROAD,IMMINGHAM,City,district,State,DN40 1QR,UK,2009-01-24,2033-01-01 +Valid_NHS,9650351760,HORACE,GRAHAM,male,2003-10-24,1 MICHAELSON AVENUE,MORECAMBE,City,district,State,LA4 6SD,UK,2005-03-18,2033-01-01 +Valid_NHS,9689182854,MAGGIE,BONNEY,female,2003-10-24,10 DOROTHY GROVE,LEIGH,City,district,State,WN7 4DD,UK,2004-05-27,2033-01-01 +Valid_NHS,9476710861,EDMOND,HEAD,male,2003-10-24,ROSE COTTAGE,PEASMARSH,City,district,State,TN31 6UX,UK,2012-08-03,2033-01-01 +Valid_NHS,9658122302,MIKE,HARRIS,male,2003-10-24,1 ST. EDMUNDS COURT,LEEDS,City,district,State,LS8 1EZ,UK,2018-01-13,2033-01-01 +Valid_NHS,9690792091,NANCY,STRAIN,unknown,2003-10-24,1 THE WILLOWS,NORMANBY,City,district,State,DN15 9HT,UK,2018-01-13,2033-01-01 +Valid_NHS,9691860511,LEAH,HELSBY,female,2003-10-24,11 WATER HILL,SOWERBY BRIDGE,City,district,State,HX6 2UE,UK,2011-01-06,2033-01-01 +Valid_NHS,9726779898,DENNIS,SEDDON,male,2003-10-24,39 THORNTON AVENUE,SCUNTHORPE,City,district,State,DN16 2BD,UK,2018-07-29,2033-01-01 +Valid_NHS,9486598487,LUCY,BRODIE,unknown,2003-10-24,1 THE WILLOWS,NORMANBY,City,district,State,DN15 9HT,UK,2015-04-22,2033-01-01 +Valid_NHS,5992264841,TRISTEN,QUINN,female,2003-10-24,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-10-24,2033-01-01 +Valid_NHS,9657955424,GAYNOR,FILES,female,2003-10-24,4 TRENT VIEW,KEADBY,City,district,State,DN17 3BY,UK,2010-06-16,2033-01-01 +Valid_NHS,9651007419,DAPHNE,JACKS,female,2003-10-24,73 HENDERSON AVENUE,SCUNTHORPE,City,district,State,DN15 7RS,UK,2016-06-04,2033-01-01 +Valid_NHS,5992961879,REGGIE,LEHNER,female,2003-10-24,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-03-07,2033-01-01 +Valid_NHS,5993256893,GRACE,ADAMS,female,2003-10-24,48949 Ginette Islands Eastotisburgh,GB,City,district,State,CR9 2BY,UK,2023-04-22,2033-01-01 +Valid_NHS,9731733418,SOPHIE,HEALEY,female,2003-10-24,1 RAVENSCROFT ROAD,LONDON,City,district,State,W4 5EQ,UK,2022-12-27,2033-01-01 +Valid_NHS,5997587452,KATHRINE,BRUEN,female,2003-10-24,01,Moore,City,district,State,N8 7RE,UK,2024-07-25,2033-01-01 +Valid_NHS,5998589327,ALISSA,HAN,male,2003-10-24,Sydenham House,Mill Ct,City,district,State,TN24 8DN,UK,2024-10-24,2033-01-01 +Valid_NHS,9734496190,SYLVIA,HOWITT,female,2003-10-24,1 SOUTH VIEW,CLIFFORD,City,district,State,LS23 6JD,UK,2020-10-26,2033-01-01 +Valid_NHS,9734730274,HILDA,FEY,female,2003-10-24,Praed Street,London,City,district,State,W2 1NY,UK,2016-03-25,2033-01-01 +Valid_NHS,5999552141,MICHAL,JENKINS,male,2004-06-09,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-02-10,2033-01-01 +Valid_NHS,9734103202,EUGENE,SMOUT,male,2004-06-09,1 OLD DAIRY,BARROW-UPON-HUMBER,City,district,State,DN19 7ST,UK,2023-10-17,2033-01-01 +Valid_NHS,9733102105,HAZEL,TRASK,female,2004-06-09,HART COTTAGE,GREENGATE LANE,City,district,State,DN19 7HT,UK,2007-11-29,2033-01-01 +Valid_NHS,9731730990,SABRA,OCHS,female,2004-06-09,1 PRIORY WALK,LANCASTER,City,district,State,LA1 3QN,UK,2022-02-08,2033-01-01 +Valid_NHS,9730738017,JULIET,REES,female,2004-06-09,10 INGLEWOOD COURT,SCUNTHORPE,City,district,State,DN16 3LE,UK,2016-10-04,2033-01-01 +Valid_NHS,9486151008,NEIL,PAICE,male,2004-06-09,INGLEWOOD,ALDCLIFFE,City,district,State,LA1 5AR,UK,2005-05-01,2033-01-01 +Valid_NHS,9651810793,RENA,CROFT,female,2004-06-09,51A,PRINCES AVENUE,City,district,State,HU5 3QY,UK,2005-09-15,2033-01-01 +Valid_NHS,9726179734,HEIDI,GRICE,unknown,2004-06-09,3 WORCESTER AVENUE,GRIMSBY,City,district,State,DN34 5EZ,UK,2006-10-29,2033-01-01 +Valid_NHS,9656860659,TINA,DRURY,female,2004-06-09,1 EVANS PLACE,BILSTON,City,district,State,WV14 6LF,UK,2015-11-17,2033-01-01 +Valid_NHS,9490237205,LESLIE,CLOCK,male,2004-06-09,FELL END HOUSE,HALE,City,district,State,LA7 7BW,UK,2004-11-22,2033-01-01 +Valid_NHS,9726417775,GLENDA,BAGGS,unknown,2004-06-09,2 BRANSDALE WAY,GRIMSBY,City,district,State,DN37 9BL,UK,2016-03-13,2033-01-01 +Valid_NHS,9476325119,ALBERT,KITSON,male,2004-06-09,25 HARTBURN DRIVE,NEWCASTLE UPON TYNE,City,district,State,NE5 1TA,UK,2015-05-07,2033-01-01 +Valid_NHS,9651731494,LANCE,JOSSE,male,2004-06-09,153 BRISTOL ROAD,HULL,City,district,State,HU5 5XP,UK,2017-01-10,2033-01-01 +Valid_NHS,9475732144,DAWN,HESLBY,unknown,2004-06-09,11 CONISTON DRIVE,KENDAL,City,district,State,LA9 6LE,UK,2010-04-11,2033-01-01 +Valid_NHS,9661388229,CARL,HOWSE,male,2004-06-09,1 HIGH BURGAGE,WINTERINGHAM,City,district,State,DN15 9NE,UK,2017-03-22,2033-01-01 +Valid_NHS,9471137969,ALDOUS,BLAIR,male,2004-06-09,77 WENTWORTH ROAD,BLACKER HILL,City,district,State,S74 0RL,UK,2016-02-08,2033-01-01 +Valid_NHS,9481152782,LIZZY,PEILL,female,2004-06-09,30 STANLEY ROAD,SALFORD,City,district,State,M7 4ES,UK,2009-10-09,2033-01-01 +Valid_NHS,9486095027,CALVIN,KINCLA,male,2004-06-09,2 SEA VIEW,ULVERSTON,City,district,State,LA12 7EX,UK,2004-06-22,2033-01-01 +Valid_NHS,9650918825,MEGAN,PENTON,female,2004-06-09,10 SOUTH DALE CLOSE,KIRTON LINDSEY,City,district,State,DN21 4BS,UK,2016-07-13,2033-01-01 +Valid_NHS,9661365695,YVETTE,GOUGH,female,2004-06-09,25 ST. HELENS LANE,LEEDS,City,district,State,LS16 8BR,UK,2007-04-30,2033-01-01 +Valid_NHS,9690801066,SIMON,BIRCH,male,2004-06-09,44-46,HIGH STREET,City,district,State,DN15 6SX,UK,2010-10-10,2033-01-01 +Valid_NHS,9457816242,RAIN,MACLARTY,unknown,2004-06-09,27 THE DRIVE,ADEL,City,district,State,LS16 6BQ,UK,2013-12-22,2033-01-01 +Valid_NHS,9473200218,LUANA,PRUETT,female,2004-06-09,10 EVERILL GATE LANE,WOMBWELL,City,district,State,S73 0SF,UK,2016-02-08,2033-01-01 +Valid_NHS,9694703948,GURPREET,SIHOTA,female,2004-06-09,2 CHURCH CLOSE,WALTHAM,City,district,State,DN37 0PQ,UK,2015-04-08,2033-01-01 +Valid_NHS,9470072243,DORETTA,CONDLIFFE,unknown,2004-06-09,1 INFIELD GARDENS,BARROW-IN-FURNESS,City,district,State,LA13 9JW,UK,2016-02-08,2033-01-01 +Valid_NHS,5990072120,LUCY,PALMER,female,2004-06-09,Bickley House,Freasley,City,district,State,B78 2EZ,UK,2004-06-09,2033-01-01 +Valid_NHS,9652075426,TESSA,HENON,female,2004-06-09,103 MELDRETH ROAD,WHADDON,City,district,State,SG8 5RS,UK,2007-11-21,2033-01-01 +Valid_NHS,9691578182,EDITH,NEWNES,unknown,2004-06-09,3 REDCOMBE LANE,BRIGG,City,district,State,DN20 8AU,UK,2013-10-29,2033-01-01 +Valid_NHS,9465721964,TISHA,HICKLIN,female,2004-06-09,58 CEMETERY ROAD,SCUNTHORPE,City,district,State,DN16 1EA,UK,2015-07-21,2033-01-01 +Valid_NHS,9471016428,BAILEY,REEDS,male,2004-06-09,ELLAR CARR HOUSE,ELLAR CARR ROAD,City,district,State,BD13 5HX,UK,2016-02-08,2033-01-01 +Valid_NHS,9486534926,CARRIE,SANDS,unknown,2004-06-09,74 LINKSIDE,BRETTON,City,district,State,PE3 8PA,UK,2004-12-17,2033-01-01 +Valid_NHS,9490432199,JORDAN,TEAT,male,2004-06-09,MEALBANK COTTAGE,TATHAM,City,district,State,LA2 8NG,UK,2012-01-23,2033-01-01 +Valid_NHS,9471149150,CLYDE,POTTS,male,2004-06-09,10 WHINSTONE DRIVE,STAINTON,City,district,State,TS8 9AT,UK,2016-02-08,2033-01-01 +Valid_NHS,9650868224,DEREK,ANWYLL,male,2004-06-09,143 CARR LANE,GRIMSBY,City,district,State,DN32 8JN,UK,2016-10-23,2033-01-01 +Valid_NHS,9692986942,CLARA,MCEWEN,female,2004-06-09,16 WENTWORTH CRESCENT,NEW HOLLAND,City,district,State,DN19 7SB,UK,2017-06-13,2033-01-01 +Valid_NHS,9481295737,LOUIE,BERRY,male,2004-06-09,11 CARR BRIDGE VIEW,LEEDS,City,district,State,LS16 7BR,UK,2008-01-26,2033-01-01 +Valid_NHS,9661224757,NOEL,MELLOR,male,2004-06-09,11 NORTH STREET,CROWLE,City,district,State,DN17 4NB,UK,2004-11-30,2033-01-01 +Valid_NHS,9650146350,FAYE,CAPNER,female,2004-06-09,1 HOPE STREET,BARROW-IN-FURNESS,City,district,State,LA14 2LX,UK,2016-01-26,2033-01-01 +Valid_NHS,9650483829,DEANNA,CUBBON,female,2004-06-09,10 MULBERRY WAY,BARROW-IN-FURNESS,City,district,State,LA13 0RH,UK,2017-02-22,2033-01-01 +Valid_NHS,9692142167,CASEY,USHER,unknown,2004-06-09,100 BURRINGHAM ROAD,SCUNTHORPE,City,district,State,DN17 2DE,UK,2007-03-05,2033-01-01 +Valid_NHS,9476115084,MARIA,PROUT,female,2004-06-09,1 PELHAM CLOSE,BARTON-UPON-HUMBER,City,district,State,DN18 5NW,UK,2004-07-23,2033-01-01 +Valid_NHS,9484959083,JESSIE,GIBNEY,female,2004-06-09,101 MAIN ROAD,GALGATE,City,district,State,LA2 0LA,UK,2007-06-08,2033-01-01 +Valid_NHS,9459999190,MRIGANKAMOULI,MURTUGUDDE,unknown,2004-06-09,PYEMOSS HOUSE,WIGGLESWORTH,City,district,State,BD23 4SB,UK,2013-12-22,2033-01-01 +Valid_NHS,9651788712,DOLLY,ADCOCK,unknown,2004-06-09,10 STEETON AVENUE,HULL,City,district,State,HU6 7AZ,UK,2008-06-19,2033-01-01 +Valid_NHS,9476347384,MORIAH,BERG,female,2004-06-09,1 ARDEN AVENUE,NEWCASTLE UPON TYNE,City,district,State,NE3 5TS,UK,2004-08-02,2033-01-01 +Valid_NHS,9461389086,CHILE,KELLETT,male,2004-06-09,AYTON HALL FARM,LOW GREEN,City,district,State,TS9 6PS,UK,2013-12-22,2033-01-01 +Valid_NHS,9725837827,ISAAC,MILMIN,male,2004-06-09,134 FAIRWAY,WALTHAM,City,district,State,DN37 0NG,UK,2022-01-16,2033-01-01 +Valid_NHS,9473130635,DAYTON,HOPWOOD,male,2004-06-09,1 FERN CLOSE,HUNTINGTON,City,district,State,YO32 9PA,UK,2016-02-08,2033-01-01 +Valid_NHS,9651829354,ERIC,MCGURK,male,2004-06-09,1 CORONATION ROAD SOUTH,HULL,City,district,State,HU5 5QL,UK,2010-07-23,2033-01-01 +Valid_NHS,9726112974,ALBERT,ASNIP,male,2004-06-09,51 CASTLE STREET,GRIMSBY,City,district,State,DN32 7LG,UK,2004-10-20,2033-01-01 +Valid_NHS,5992774947,KEVIN,WALSH,female,2004-06-09,Bad Oyenhausen,SCHULSTRASSE,City,district,State,ZZ99 3AZ,UK,2023-02-03,2033-01-01 +Valid_NHS,9728623062,DILYS,DAVITT,female,2004-06-09,1 KEAN PLACE,ALVASTON,City,district,State,DE24 8PP,UK,2009-02-26,2033-01-01 +Valid_NHS,5993499796,COLTON,FRAMI,male,2004-06-09,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-06-09,2033-01-01 +Valid_NHS,5993502460,JESENIA,MCCLURE,male,2004-06-09,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-06-09,2033-01-01 +Valid_NHS,5993518456,MATHEW,POWLOWSKI,male,2004-06-09,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-06-09,2033-01-01 +Valid_NHS,5993508108,MOSHE,BLANDA,male,2004-06-09,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-06-09,2033-01-01 +Valid_NHS,5994627488,HERMINE,JAKUBOWSKI,male,2004-06-09,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-09-20,2033-01-01 +Valid_NHS,9732411457,ALISON,COLE,unknown,2004-06-09,1 WALESBY ROAD,SCUNTHORPE,City,district,State,DN17 2JP,UK,2007-05-06,2033-01-01 +Valid_NHS,9734536095,GWEN,SCULLY,unknown,2004-06-09,1 OAKFIELD GARDENS,CARSHALTON,City,district,State,SM5 1NY,UK,2005-09-10,2033-01-01 +Valid_NHS,9734728644,MAUDE,WOOKEY,female,2004-06-09,Praed Street,London,City,district,State,W2 1NY,UK,2007-05-12,2033-01-01 +Valid_NHS,5999752582,BOBBY,PACOCHA,male,2004-06-09,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-02-21,2033-01-01 +Valid_NHS,9733947318,ZEPHRA,GRIMES,female,2004-11-22,1 PARK EDGE CLOSE,LEEDS,City,district,State,LS8 2LP,UK,2016-04-21,2033-01-01 +Valid_NHS,9733231525,DAVINA,GANTES,female,2004-11-22,10 FERRY ROAD,ALTHORPE,City,district,State,DN17 3HS,UK,2017-07-13,2033-01-01 +Valid_NHS,9732314400,TOM,HOLDER,male,2004-11-22,1 COUNCIL HOUSE,HIGH STREET,City,district,State,DN17 3AH,UK,2006-01-21,2033-01-01 +Valid_NHS,9732245115,HELENE,LEE,unknown,2004-11-22,305 ASHBY HIGH STREET,SCUNTHORPE,City,district,State,DN16 2RY,UK,2012-03-03,2033-01-01 +Valid_NHS,5996634406,WOODROW,DONNELLY,male,2004-11-22,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,ZZ99 4FZ,UK,2024-04-05,2033-01-01 +Valid_NHS,9732003057,EVE,DEACON,unknown,2004-11-22,2 BELWOOD VILLAS,BELTOFT,City,district,State,DN9 1LZ,UK,2014-03-23,2033-01-01 +Valid_NHS,9729917043,QITARAH,MUIRIMI,female,2004-11-22,10 ASHLIN COURT,MESSINGHAM,City,district,State,DN17 3TB,UK,2011-03-29,2033-01-01 +Valid_NHS,5993558660,DARLENA,MURAZIK,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2023-06-20,2033-01-01 +Valid_NHS,5993307722,SHANITA,CASSIN,female,2004-11-22,99613,LONDON,City,district,State,F78 0LB,UK,2023-05-05,2033-01-01 +Valid_NHS,9460930808,AMANDA,VLASSI,female,2004-11-22,2 POTTS LANE,CROWLE,City,district,State,DN17 4DP,UK,2013-12-22,2033-01-01 +Valid_NHS,9469733371,ALMA,AKAUCH,female,2004-11-22,1 THIRLMERE COURT,LANCASTER,City,district,State,LA1 3LQ,UK,2016-02-08,2033-01-01 +Valid_NHS,9477383481,LAUREN,IRVINE,female,2004-11-22,10 CASTLEBRIDGE ROAD,WOLVERHAMPTON,City,district,State,LA1 3LQ,UK,2005-09-06,2033-01-01 +Valid_NHS,9471041198,CONRAD,STODDELEY,male,2004-11-22,1 HOLLIS CLOSE,RAWMARSH,City,district,State,S62 7LX,UK,2016-02-08,2033-01-01 +Valid_NHS,9693303296,CASSIE,RODDEN,female,2004-11-22,58 SHELDON ROAD,SHEFFIELD,City,district,State,S7 1GX,UK,2015-07-22,2033-01-01 +Valid_NHS,9486589348,MINA,AYRE,female,2004-11-22,6 POND SIDE,WOOTTON,City,district,State,DN39 6SD,UK,2008-06-06,2033-01-01 +Valid_NHS,9661234825,AMY,INVALID=CHARSURNAME,unknown,2004-11-22,10 CAENBY ROAD,SCUNTHORPE,City,district,State,DN17 2EW,UK,2011-08-13,2033-01-01 +Valid_NHS,5990076428,ALEX,DEKKERS,male,2004-11-22,36 Nethersole Street,Polesworth,City,district,State,B78 1EE,UK,2004-11-22,2033-01-01 +Valid_NHS,9471114527,KAYCEE,WHITTER,female,2004-11-22,161 HEDGELEY ROAD,HEBBURN,City,district,State,NE31 1HD,UK,2016-02-08,2033-01-01 +Valid_NHS,9486250456,AGNES,ARKLEY,unknown,2004-11-22,1 MORETON GREEN,HEYSHAM,City,district,State,LA3 2FD,UK,2011-11-26,2033-01-01 +Valid_NHS,9692364216,LI RONG,NGAO,unknown,2004-11-22,12 GLANVILLE AVENUE,SCUNTHORPE,City,district,State,DN17 1DE,UK,2019-07-20,2033-01-01 +Valid_NHS,9490210587,BLANCH,FREEAR,unknown,2004-11-22,10 ST. ANNES CLOSE,AMBLESIDE,City,district,State,LA22 9HB,UK,2008-07-16,2033-01-01 +Valid_NHS,9471185823,ROSE,WEBZELL,female,2004-11-22,2 WESTWOOD WAY,BEVERLEY,City,district,State,HU17 8GE,UK,2016-02-08,2033-01-01 +Valid_NHS,9692519430,DAN,PRIEST,male,2004-11-22,ASH TREE FARM BUNGALOW,JERICHO LANE,City,district,State,DN40 3PZ,UK,2006-12-26,2033-01-01 +Valid_NHS,9692696340,ENOCH,SWEDE,male,2004-11-22,10 CHURCHFIELD ROAD,SCUNTHORPE,City,district,State,DN16 3DH,UK,2016-03-19,2033-01-01 +Valid_NHS,9473794568,SUZY,ONAGORUWA,female,2004-11-22,1 WOODLEIGH GROVE,HUDDERSFIELD,City,district,State,HD4 7AN,UK,2016-02-08,2033-01-01 +Valid_NHS,5990473974,MARLOWE,CLARKE,male,2004-11-22,CAMPUS CITY NEW CAMPUS 41 BERKELEY ROAD,WESTBURY PARK,City,district,State,M16 0RA,UK,2021-06-09,2033-01-01 +Valid_NHS,9461058810,SHARISE,ERNELEY,unknown,2004-11-22,11 WOLSTON CLOSE,BRADFORD,City,district,State,BD4 0JY,UK,2013-12-22,2033-01-01 +Valid_NHS,9485323204,BRIAN,LAMB,male,2004-11-22,2 ST. NICHOLAS LANE,BOLTON LE SANDS,City,district,State,LA5 8BT,UK,2014-10-12,2033-01-01 +Valid_NHS,9651728302,PHOEBE,GRAY,female,2004-11-22,100 WHARNCLIFFE STREET,HULL,City,district,State,HU5 3NA,UK,2013-09-18,2033-01-01 +Valid_NHS,9692646343,JULIE,SHORT,female,2004-11-22,1 ORCHARD DRIVE,WINTERINGHAM,City,district,State,DN15 9PG,UK,2008-11-03,2033-01-01 +Valid_NHS,9476325933,ROBIN,GLUCK,male,2004-11-22,1 NORMOUNT AVENUE,NEWCASTLE UPON TYNE,City,district,State,NE4 8AR,UK,2015-08-13,2033-01-01 +Valid_NHS,9650243879,SUSAN,OUSEY,unknown,2004-11-22,10 HAMPSFELL DRIVE,MORECAMBE,City,district,State,LA4 4TT,UK,2013-10-20,2033-01-01 +Valid_NHS,9726526124,LAUREN,LYNN,unknown,2004-11-22,1 PARK AVENUE,GRIMSBY,City,district,State,DN32 0DG,UK,2009-12-01,2033-01-01 +Valid_NHS,9470111028,RATANJOT,SOLKAR,female,2004-11-22,11 MARSHALL AVENUE,GRIMSBY,City,district,State,DN34 4AJ,UK,2016-02-08,2033-01-01 +Valid_NHS,9691842637,MARA,HYMAN,unknown,2004-11-22,99 Any Street,Anytown,City,district,State,DN34 4AJ,UK,2012-03-06,2033-01-01 +Valid_NHS,9692755290,DEBBIE,MARTIN,unknown,2004-11-22,4 APPLEFIELDS,WRAWBY,City,district,State,DN20 8GB,UK,2010-04-19,2033-01-01 +Valid_NHS,5990468571,ELLIS,PHILLIPS,male,2004-11-22,CAMPUS CITY NEW CAMPUS 41 BERKELEY ROAD,WESTBURY PARK,City,district,State,M16 0RA,UK,2021-06-07,2033-01-01 +Valid_NHS,9484926150,BRUCE,ALDIS,male,2004-11-22,11 BUTTS BECK,DALTON-IN-FURNESS,City,district,State,LA15 8EP,UK,2008-11-13,2033-01-01 +Valid_NHS,9650890262,KATIE,FLING,female,2004-11-22,1 WENTWORTH ROAD,GRIMSBY,City,district,State,DN34 4AR,UK,2009-06-05,2033-01-01 +Valid_NHS,9490233773,SOPHIE,CHURCH,female,2004-11-22,35 WESTHILLS DRIVE,ULVERSTON,City,district,State,LA12 9QA,UK,2013-07-06,2033-01-01 +Valid_NHS,9661267480,AMANDA,ANWYLL,female,2004-11-22,10 FALL SPRING GARDENS,HOLYWELL GREEN,City,district,State,HX4 9PB,UK,2011-11-02,2033-01-01 +Valid_NHS,9464911719,TARANA,MALLESHI,female,2004-11-22,1 WOODLANDS DRIVE,MORLEY,City,district,State,LS27 9QZ,UK,2015-01-30,2033-01-01 +Valid_NHS,9693600835,CHLOE,PECK,unknown,2004-11-22,TRURO COURT,TORRINGTON ROAD,City,district,State,DN17 1UU,UK,2015-01-17,2033-01-01 +Valid_NHS,5992420096,REID,WAELCHI,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992426515,JANETTA,STROMAN,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992438157,LELAND,BORER,female,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992421882,MARION,MOORE,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992437002,KELSEY,KERR,female,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992419098,GEORGIANA,STOKES,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992424067,ELLIS,UNDERWOOD,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992425845,LAVONE,NITZSCHE,female,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992426795,DARRON,JAKUBOWSKI,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992438424,VAL,VAUGHAN,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,5992429581,PAT,KREIGER,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2022-11-22,2033-01-01 +Valid_NHS,9728697481,DAISY,NELSON,female,2004-11-22,THE CONCOURSE,1 POULTRY,City,district,State,EC2R 8EN,UK,2010-11-14,2033-01-01 +Valid_NHS,5995362089,DORSEY,HYATT,male,2004-11-22,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-22,2033-01-01 +Valid_NHS,5995377418,DEVORAH,LOCKMAN,male,2004-11-22,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-22,2033-01-01 +Valid_NHS,5995368966,LAURIE,ADAMS,male,2004-11-22,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-22,2033-01-01 +Valid_NHS,5995371223,EMIL,HILLL,male,2004-11-22,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-22,2033-01-01 +Valid_NHS,5995370154,LOUISA,LABADIE,male,2004-11-22,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-22,2033-01-01 +Valid_NHS,5995373404,RANEE,HAMILL,male,2004-11-22,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-22,2033-01-01 +Valid_NHS,5995369601,SELENE,BARTON,male,2004-11-22,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2023-11-22,2033-01-01 +Valid_NHS,5995399470,NELLE,SMITH,male,2004-11-22,Campus City New Campus 41 Berkeley Road,Westbury Park,City,district,State,M16 0RA,UK,2023-11-24,2033-01-01 +Valid_NHS,5997447170,CATRICE,BARTOLETTI,male,2004-11-22,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2024-07-11,2033-01-01 +Valid_NHS,5997758915,SANTOS,RUNOLFSSON,male,2004-11-22,Clapton Station,High Street Mall,City,district,State,SW1H 0BT,UK,2024-08-09,2033-01-01 +Valid_NHS,5997789551,LUCA,MAZ,female,2004-11-22,Sydenham House,Mill Ct,City,district,State,TN24 8DN,UK,2024-08-13,2033-01-01 +Valid_NHS,9733570265,EDGAR,PKZ-NOV-PDS-ALPHA-ZERO,male,2004-11-22,1 PETERBOROUGH ROAD,SCUNTHORPE,City,district,State,DN16 2DT,UK,2019-01-02,2033-01-01 +Valid_NHS,9734726072,ROSLYN,WEBB,female,2004-11-22,Praed Street,London,City,district,State,W2 1NY,UK,2013-02-21,2033-01-01 +Valid_NHS,9000029473,BAILEY,WELLER,female,2005-03-14,02,Test Street,City,district,State,N8 7RE,UK,2025-03-14,2033-01-01 +Valid_NHS,9000023955,DALE,VAUGHAN,female,2005-03-14,02,Test Street,City,district,State,N8 7RE,UK,2025-03-14,2033-01-01 +Valid_NHS,9000031494,MONTY,SPINKA,male,2005-03-14,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-14,2033-01-01 +Valid_NHS,9000031516,LIESELOTTE,BOYER,male,2005-03-14,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-14,2033-01-01 +Valid_NHS,9000024056,CHI,CONN,male,2005-03-14,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-14,2033-01-01 +Valid_NHS,9000036313,JOSHUA,HYATT,male,2005-03-14,Bad Oyenhausen,Notgiven,City,district,State,ZZ99 4QZ,UK,2025-03-14,2033-01-01 +Valid_NHS,9000046696,GLENN,ROWE,male,2005-03-14,Moore 01 Bishop,Essex,City,district,State,N8 7RE,UK,2025-03-14,2033-01-01 +Valid_NHS,9734891650,HETTY,DAWKIN,female,2005-03-14,1 GARDEN COURT,EPWORTH,City,district,State,DN9 1GA,UK,2016-06-12,2033-01-01 +Valid_NHS,9734281674,LANDON,EWART,male,2005-03-14,1186 THE MOORS,KIDLINGTON,City,district,State,OX5 2AD,UK,2021-09-29,2033-01-01 +Valid_NHS,9734353055,BETSY,HOPSON,female,2005-03-14,1186 THE MOORS,KIDLINGTON,City,district,State,OX4 4LX,UK,2013-01-28,2033-01-01 +Valid_NHS,9734107720,SARAH,PEILL,female,2005-03-14,1 DELAMERE,WYNYARD,City,district,State,TS22 5GH,UK,2016-11-10,2033-01-01 \ No newline at end of file diff --git a/tests/e2e_automation/poetry.lock b/tests/e2e_automation/poetry.lock new file mode 100644 index 0000000000..0ccae2f0a0 --- /dev/null +++ b/tests/e2e_automation/poetry.lock @@ -0,0 +1,1055 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "allure-pytest" +version = "2.14.3" +description = "Allure pytest integration" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "allure_pytest-2.14.3-py3-none-any.whl", hash = "sha256:9704dea12f98991bc9e13be4ce9f90fe703beaff90e4741d67ae64c9815336c5"}, + {file = "allure_pytest-2.14.3.tar.gz", hash = "sha256:6d02a129867c7ef8b6943721d2be3141e3d1c3551741aff17b6afee64cd92b60"}, +] + +[package.dependencies] +allure-python-commons = "2.14.3" +pytest = ">=4.5.0" + +[[package]] +name = "allure-python-commons" +version = "2.14.3" +description = "Contains the API for end users as well as helper functions and classes to build Allure adapters for Python test frameworks" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "allure_python_commons-2.14.3-py3-none-any.whl", hash = "sha256:7bcf6a2eff69e6f0eacd8681e923f0a9dfbdd86cb8e20d8f93288057f9fb7cb1"}, + {file = "allure_python_commons-2.14.3.tar.gz", hash = "sha256:2dcaa87dd6b0b41902f60fe17e2653d3762788f3998ca4cf721dabfa8342aa07"}, +] + +[package.dependencies] +attrs = ">=16.0.0" +pluggy = ">=0.4.0" + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "aws-sso-lite" +version = "0.0.4" +description = "A lightweight package to do aws sso without aws cli" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "aws_sso_lite-0.0.4-py3-none-any.whl", hash = "sha256:13f62d397fef5bf409929f027e092e345f791ff05ed93cfbacc9cdb0d2a350fd"}, + {file = "aws_sso_lite-0.0.4.tar.gz", hash = "sha256:7f3e8ae94c8bcb36f8c1340f9092499230e4564dff164df9d3a94e07124c702c"}, +] + +[[package]] +name = "boto3" +version = "1.38.46" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "boto3-1.38.46-py3-none-any.whl", hash = "sha256:9c8e88a32a6465e5905308708cff5b17547117f06982908bdfdb0108b4a65079"}, + {file = "boto3-1.38.46.tar.gz", hash = "sha256:d1ca2b53138afd0341e1962bd52be6071ab7a63c5b4f89228c5ef8942c40c852"}, +] + +[package.dependencies] +botocore = ">=1.38.46,<1.39.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.13.0,<0.14.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.38.46" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "botocore-1.38.46-py3-none-any.whl", hash = "sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b"}, + {file = "botocore-1.38.46.tar.gz", hash = "sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.23.8)"] + +[[package]] +name = "certifi" +version = "2026.1.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "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"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "gherkin-official" +version = "29.0.0" +description = "Gherkin parser (official, by Cucumber team)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "gherkin_official-29.0.0-py3-none-any.whl", hash = "sha256:26967b0d537a302119066742669e0e8b663e632769330be675457ae993e1d1bc"}, + {file = "gherkin_official-29.0.0.tar.gz", hash = "sha256:dbea32561158f02280d7579d179b019160d072ce083197625e2f80a6776bb9eb"}, +] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "lxml" +version = "5.4.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, + {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, + {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, + {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, + {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, + {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, + {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, + {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, + {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, + {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, + {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, + {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, + {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, + {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, + {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, + {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, + {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, + {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, + {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, + {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, + {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, + {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11,<3.1.0)"] + +[[package]] +name = "mako" +version = "1.3.10" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "numpy" +version = "2.4.1" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5"}, + {file = "numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425"}, + {file = "numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba"}, + {file = "numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501"}, + {file = "numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a"}, + {file = "numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509"}, + {file = "numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc"}, + {file = "numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82"}, + {file = "numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0"}, + {file = "numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574"}, + {file = "numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73"}, + {file = "numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2"}, + {file = "numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8"}, + {file = "numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a"}, + {file = "numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0"}, + {file = "numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c"}, + {file = "numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02"}, + {file = "numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162"}, + {file = "numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9"}, + {file = "numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f"}, + {file = "numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87"}, + {file = "numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8"}, + {file = "numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b"}, + {file = "numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f"}, + {file = "numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9"}, + {file = "numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e"}, + {file = "numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5"}, + {file = "numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8"}, + {file = "numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c"}, + {file = "numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2"}, + {file = "numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d"}, + {file = "numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb"}, + {file = "numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5"}, + {file = "numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7"}, + {file = "numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d"}, + {file = "numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15"}, + {file = "numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9"}, + {file = "numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2"}, + {file = "numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505"}, + {file = "numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2"}, + {file = "numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4"}, + {file = "numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510"}, + {file = "numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261"}, + {file = "numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc"}, + {file = "numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3"}, + {file = "numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220"}, + {file = "numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee"}, + {file = "numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556"}, + {file = "numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844"}, + {file = "numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3"}, + {file = "numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205"}, + {file = "numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745"}, + {file = "numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d"}, + {file = "numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df"}, + {file = "numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f"}, + {file = "numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0"}, + {file = "numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c"}, + {file = "numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93"}, + {file = "numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42"}, + {file = "numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01"}, + {file = "numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b"}, + {file = "numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a"}, + {file = "numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2"}, + {file = "numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295"}, + {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1"}, + {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344"}, + {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e"}, + {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426"}, + {file = "numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696"}, + {file = "numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be"}, + {file = "numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33"}, + {file = "numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690"}, +] + +[[package]] +name = "oath" +version = "1.4.4" +description = "Python implementation of the three main OATH specifications: HOTP, TOTP and OCRA" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "oath-1.4.4-py3-none-any.whl", hash = "sha256:503092f388f041f91737f6b3bd5b83e8cf3f40c7d9bc87bcfbfac33e0ae6d685"}, + {file = "oath-1.4.4.tar.gz", hash = "sha256:bd6b20d20f2c4e3f53523ee900211dca75aeeca72f4f5a9fd8dcacc175fe1c0b"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pandas" +version = "2.3.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, +] + +[package.dependencies] +numpy = {version = ">=1.23.2", markers = "python_version == \"3.11\""} +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "parse" +version = "1.20.2" +description = "parse() is the opposite of format()" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, + {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, +] + +[[package]] +name = "parse-type" +version = "0.6.6" +description = "Simplifies to build parse types based on the parse module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,>=2.7" +groups = ["main"] +files = [ + {file = "parse_type-0.6.6-py2.py3-none-any.whl", hash = "sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c"}, + {file = "parse_type-0.6.6.tar.gz", hash = "sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2"}, +] + +[package.dependencies] +parse = {version = ">=1.18.0", markers = "python_version >= \"3.0\""} +six = ">=1.15" + +[package.extras] +develop = ["build (>=0.5.1)", "coverage (>=4.4)", "pylint", "pytest (<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-cov", "pytest-html (>=1.19.0)", "ruff ; python_version >= \"3.7\"", "setuptools", "setuptools-scm", "tox (>=2.8,<4.0)", "twine (>=1.13.0)", "virtualenv (<20.22.0) ; python_version <= \"3.6\"", "virtualenv (>=20.0.0) ; python_version > \"3.6\"", "wheel"] +docs = ["Sphinx (>=1.6)", "sphinx_bootstrap_theme (>=0.6.0)"] +testing = ["pytest (<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-html (>=1.19.0)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +optional = false +python-versions = ">=3.6.1" +groups = ["main"] +files = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-bdd" +version = "8.1.0" +description = "BDD for pytest" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest_bdd-8.1.0-py3-none-any.whl", hash = "sha256:2124051e71a05ad7db15296e39013593f72ebf96796e1b023a40e5453c47e5fb"}, + {file = "pytest_bdd-8.1.0.tar.gz", hash = "sha256:ef0896c5cd58816dc49810e8ff1d632f4a12019fb3e49959b2d349ffc1c9bfb5"}, +] + +[package.dependencies] +gherkin-official = ">=29.0.0,<30.0.0" +Mako = "*" +packaging = "*" +parse = "*" +parse-type = "*" +pytest = ">=7.0.0" +typing-extensions = "*" + +[[package]] +name = "pytest-check" +version = "2.5.4" +description = "A pytest plugin that allows multiple failures per test." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest_check-2.5.4-py3-none-any.whl", hash = "sha256:08c5f0ef40717ef8e55ee09d65dd7c919502db6eadb37a09818c12c28a57ad50"}, + {file = "pytest_check-2.5.4.tar.gz", hash = "sha256:33d87e28d5e49217f413277e1e0d267cd66c90a85a208944c44312c9c8e4ff74"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +dev = ["build", "tox", "tox-uv"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.1.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[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 = "s3transfer" +version = "0.13.1" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"}, + {file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"}, +] + +[package.dependencies] +botocore = ">=1.37.4,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "tzdata" +version = "2025.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[metadata] +lock-version = "2.1" +python-versions = "~3.11" +content-hash = "90f21b931a1595639b807e7962b2188f1e65c26d300c642c5de5a18d09a58b24" diff --git a/tests/e2e_automation/pyproject.toml b/tests/e2e_automation/pyproject.toml new file mode 100644 index 0000000000..626a16f8fb --- /dev/null +++ b/tests/e2e_automation/pyproject.toml @@ -0,0 +1,28 @@ +[tool.poetry] +name = "e2e_automation" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "~3.11" +allure-pytest = "~2.14.2" +allure-python-commons = "~2.14.2" +aws-sso-lite = "~0.0.4" +boto3 = "~1.38.44" +botocore = "~1.38.44" +lxml = "~5.4.0" +oath = "^1.4.4" +pandas = "~2.3.0" +pydantic = "~1.8.2" +pytest = "~8.3.5" +pytest-bdd = "~8.1.0" +pytest-check = "~2.5.3" +python-dotenv = "~1.1.0" +requests = "~2.32.3" +typing_extensions = "~4.14.0" + +[build-system] +requires = ["poetry-core ~= 1.5.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/e2e_automation/pytest.ini b/tests/e2e_automation/pytest.ini new file mode 100644 index 0000000000..e325dcf79d --- /dev/null +++ b/tests/e2e_automation/pytest.ini @@ -0,0 +1,55 @@ +# pytest.ini +[pytest] +bdd_features_base_dir = features +python_files = test_*.py + +addopts = --alluredir output/allure-results + --clean-alluredir + +markers = + allure.suite: Assigns test to a suite in Allure reports + Create_Feature: tag for create feature tests + Search_Feature: tag for search feature tests + Delete_Feature: tag for delete feature tests + Read_Feature: tag for read feature tests + Status_Feature: tag for status feature tests + Update_Feature: tag for update feature tests + Create_Batch_Feature: tag for create batch feature tests + Update_Batch_Feature: tag for update batch feature tests + Delete_Batch_Feature: tag for delete batch feature tests + Delete_cleanUp: tag for scenarios that require cleanup after execution + delete_cleanup_batch: tag for batch scenarios that require cleanup after execution + patient_id_OldNHS: tag for old NHS patient ID scenarios + patient_id_ValidNHS: tag for valid NHS patient ID scenarios + patient_id_SFlag: tag for patient id SFlag scenario + patient_id_SupersedeNhsNo: tag for patient id Supersede NHS Number scenario + vaccine_type_RSV: tag for RSV vaccine type scenarios + vaccine_type_PNEUMOCOCCAL: tag for PNEUMOCOCCAL vaccine type scenarios + vaccine_type_FLU: tag for FLU vaccine type scenarios + patient_id_Random: tag for random selection of patient detail + patient_id_Mod11_NHS: tag for Mod11 NHS patient ID scenarios + vaccine_type_COVID: tag for COVID vaccine scenario + vaccine_type_HPV: tag for HPV vaccine type scenarios + vaccine_type_PERTUSSIS: tag for PERTUSSIS vaccine type scenarios + vaccine_type_MMR: tag for MMR vaccine type scenarios + vaccine_type_MMRV: tag for MMRV vaccine type scenarios + vaccine_type_MENACWY: tag for MENACWY vaccine type scenarios + vaccine_type_3IN1: tag for 3IN1 vaccine type scenarios + vaccine_type_SHINGLES: tag for SHINGLES vaccine type scenarios + vaccine_type_HIB: tag for HIB vaccine type scenarios + vaccine_type_HEPB: tag for HEPB vaccine type scenarios + vaccine_type_6IN1: tag for 6IN1 vaccine type scenarios + vaccine_type_4IN1: tag for 4IN1 vaccine type scenarios + vaccine_type_MENB: tag for MENB vaccine type scenarios + vaccine_type_ROTAVIRUS: tag for ROTAVIRUS vaccine type scenarios + vaccine_type_BCG: tag for BCG vaccine type scenarios + supplier_name_RAVS: tag for RAVS supplier name scenarios + supplier_name_MAVIS: tag for MAVIS supplier name scenarios + supplier_name_EMIS: tag for EMIS supplier name scenarios + supplier_name_SONAR: tag for SONAR supplier name scenarios + supplier_name_Postman_Auth: tag for Postman_Auth supplier name scenarios + supplier_name_MAVIS: tag for MAVIS supplier name scenarios + supplier_name_PINNACLE: tag for Pinnacle supplier name scenarios + supplier_name_TPP: tag for TPP supplier name scenarios + supplier_name_MEDICUS: tag for MEDICUS supplier name scenarios + supplier_name_CEGEDIM: tag for CEGEDIM supplier name scenarios diff --git a/tests/e2e_automation/src/__init__.py b/tests/e2e_automation/src/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_automation/src/dynamoDB/__init__.py b/tests/e2e_automation/src/dynamoDB/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_automation/src/dynamoDB/dynamo_db_helper.py b/tests/e2e_automation/src/dynamoDB/dynamo_db_helper.py new file mode 100644 index 0000000000..47b952a362 --- /dev/null +++ b/tests/e2e_automation/src/dynamoDB/dynamo_db_helper.py @@ -0,0 +1,709 @@ +import time +from collections import Counter +from typing import Dict, List + +import boto3 +import pytest_check as check +from boto3.dynamodb.conditions import Key +from botocore.config import Config +from utilities.api_fhir_immunization_helper import extract_practitioner_name +from utilities.date_helper import covert_to_expected_date_format, format_date_yyyymmdd, iso_to_compact +from utilities.enums import GenderCode +from utilities.vaccination_constants import PROTOCOL_DISEASE_MAP + +from src.objectModels.api_data_objects import ( + Coding, + ImmunizationReadResponse_IntTable, + Patient, + Practitioner, + ProtocolApplied, +) + +my_config = Config(region_name="eu-west-2", connect_timeout=10, read_timeout=500) + + +class DynamoDBHelper: + def __init__(self, aws_profile_name: str = None, env: str = "int"): + self.env = env + if aws_profile_name and aws_profile_name.strip(): + session = boto3.Session(profile_name=aws_profile_name) + self.dynamodb = session.resource("dynamodb", config=my_config) + else: + self.dynamodb = boto3.resource("dynamodb", config=my_config) + + def get_events_table(self): + return self.dynamodb.Table(f"imms-{self.env}-imms-events") + + def get_delta_table(self): + return self.dynamodb.Table(f"imms-{self.env}-delta") + + def get_batch_audit_table(self): + return self.dynamodb.Table(f"immunisation-batch-{self.env}-audit-table") + + +def fetch_immunization_events_detail( + aws_profile_name: str, + ImmsID: str, + env: str, +): + db = DynamoDBHelper(aws_profile_name, env) + tableImmsEvent = db.get_events_table() + + queryFetch = f"Immunization#{ImmsID}" + + response = tableImmsEvent.get_item(Key={"PK": queryFetch}) + print(f"\n Imms Event response is {response} \n") + + return response + + +def fetch_immunization_events_detail_by_IdentifierPK( + aws_profile_name: str, + IdentifierPK: str, + env: str, +): + db = DynamoDBHelper(aws_profile_name, env) + tableImmsEvent = db.get_events_table() + + response = tableImmsEvent.query( + IndexName="IdentifierGSI", + KeyConditionExpression="IdentifierPK = :pkval", + ExpressionAttributeValues={":pkval": IdentifierPK}, + ) + + print(f"\n Imms Event response is {response} \n") + + return response + + +def fetch_immunization_int_delta_detail_by_immsID(aws_profile_name: str, ImmsID: str, env: str, expected_item: int = 1): + db = DynamoDBHelper(aws_profile_name, env) + tableImmsDelta = db.get_delta_table() + + max_attempts = 5 + delay = 2 # seconds + + for attempt in range(1, max_attempts + 1): + response = tableImmsDelta.query(IndexName="ImmunisationIdIndex", KeyConditionExpression=Key("ImmsID").eq(ImmsID)) + + items = response.get("Items", []) + print(f"Attempt {attempt}: Found {len(items)} items") + + if len(items) >= expected_item: + print(f"\nFound Immunization Delta items for ImmsID={ImmsID}\n") + return items + + time.sleep(delay) + delay *= 2 + + print(f"\n❌ No items found for ImmsID={ImmsID} after {max_attempts} attempts.\n") + return [] + + +def fetch_batch_audit_table_detail(aws_profile_name: str, filename: str, env: str): + db = DynamoDBHelper(aws_profile_name, env) + tableImmsAudit = db.get_batch_audit_table() + + max_attempts = 5 + delay = 2 + + for attempt in range(1, max_attempts + 1): + response = tableImmsAudit.query(IndexName="filename_index", KeyConditionExpression=Key("filename").eq(filename)) + + items = response.get("Items", []) + print(f"Attempt {attempt}: Found {len(items)} items") + + if items: + print(f"\nFound Audit detail for filename={filename}\n") + return items + + time.sleep(delay) + delay *= 2 + + print(f"\n❌ No items found for filename={filename} after {max_attempts} attempts.\n") + return [] + + +def parse_imms_int_imms_event_response(resource: dict) -> ImmunizationReadResponse_IntTable: + contained_raw = resource.get("contained", []) + parsed_contained = [] + + for item in contained_raw: + if item.get("resourceType") == "Patient": + parsed_contained.append(Patient.parse_obj(item)) + elif item.get("resourceType") == "Practitioner": + parsed_contained.append(Practitioner.parse_obj(item)) + else: + parsed_contained.append(item) # fallback or raise error + + resource["contained"] = parsed_contained + return ImmunizationReadResponse_IntTable.parse_obj(resource) + + +def validate_imms_delta_record_with_created_event(context, create_obj, item, event_type, action_flag): + event = item[0].get("Imms") + assert event, "Imms field missing in items." + fields_to_compare = [ + ("Operation", event_type.upper(), item[0].get("Operation")), + ("SupplierSystem", context.supplier_name.lower(), item[0].get("SupplierSystem").lower()), + ("VaccineType", context.vaccine_type.lower(), item[0].get("VaccineType").lower()), + ("Source", "IEDS", item[0].get("Source")), + ("CONVERSION_ERRORS", [], event.get("CONVERSION_ERRORS")), + ("PERSON_FORENAME", create_obj.contained[1].name[0].given[0], event.get("PERSON_FORENAME")), + ("PERSON_SURNAME", create_obj.contained[1].name[0].family, event.get("PERSON_SURNAME")), + ("NHS_NUMBER", create_obj.contained[1].identifier[0].value, event.get("NHS_NUMBER")), + ("PERSON_DOB", create_obj.contained[1].birthDate.replace("-", ""), event.get("PERSON_DOB")), + ("PERSON_POSTCODE", create_obj.contained[1].address[0].postalCode, event.get("PERSON_POSTCODE")), + ("PERSON_GENDER_CODE", GenderCode[(create_obj.contained[1].gender)].value, event.get("PERSON_GENDER_CODE")), + ( + "VACCINATION_PROCEDURE_CODE", + create_obj.extension[0].valueCodeableConcept.coding[0].code, + event.get("VACCINATION_PROCEDURE_CODE"), + ), + ( + "VACCINATION_PROCEDURE_TERM", + create_obj.extension[0].valueCodeableConcept.coding[0].extension[0].valueString, + event.get("VACCINATION_PROCEDURE_TERM"), + ), + ( + "VACCINE_PRODUCT_TERM", + create_obj.vaccineCode.coding[0].extension[0].valueString, + event.get("VACCINE_PRODUCT_TERM"), + ), + ("VACCINE_PRODUCT_CODE", create_obj.vaccineCode.coding[0].code, event.get("VACCINE_PRODUCT_CODE")), + ("VACCINE_MANUFACTURER", create_obj.manufacturer["display"], event.get("VACCINE_MANUFACTURER")), + ("BATCH_NUMBER", create_obj.lotNumber, event.get("BATCH_NUMBER")), + ("RECORDED_DATE", create_obj.recorded[:10].replace("-", ""), event.get("RECORDED_DATE")), + ("EXPIRY_DATE", create_obj.expirationDate.replace("-", ""), event.get("EXPIRY_DATE")), + ("DOSE_SEQUENCE", str(create_obj.protocolApplied[0].doseNumberPositiveInt), event.get("DOSE_SEQUENCE")), + ("DOSE_UNIT_TERM", create_obj.doseQuantity.unit, event.get("DOSE_UNIT_TERM")), + ("DOSE_UNIT_CODE", create_obj.doseQuantity.code, event.get("DOSE_UNIT_CODE")), + ( + "SITE_OF_VACCINATION_TERM", + create_obj.site.coding[0].extension[0].valueString, + event.get("SITE_OF_VACCINATION_TERM"), + ), + ("SITE_OF_VACCINATION_CODE", create_obj.site.coding[0].code, event.get("SITE_OF_VACCINATION_CODE")), + ("DOSE_AMOUNT", create_obj.doseQuantity.value, float(event.get("DOSE_AMOUNT"))), + ("PRIMARY_SOURCE", str(create_obj.primarySource).upper(), event.get("PRIMARY_SOURCE")), + ( + "ROUTE_OF_VACCINATION_TERM", + create_obj.route.coding[0].extension[0].valueString, + event.get("ROUTE_OF_VACCINATION_TERM"), + ), + ("ROUTE_OF_VACCINATION_CODE", create_obj.route.coding[0].code, event.get("ROUTE_OF_VACCINATION_CODE")), + ("ACTION_FLAG", action_flag, event.get("ACTION_FLAG")), + ("DATE_AND_TIME", iso_to_compact(create_obj.occurrenceDateTime), event.get("DATE_AND_TIME")), + ("UNIQUE_ID", create_obj.identifier[0].value, event.get("UNIQUE_ID")), + ("UNIQUE_ID_URI", create_obj.identifier[0].system, event.get("UNIQUE_ID_URI")), + ( + "PERFORMING_PROFESSIONAL_SURNAME", + create_obj.contained[0].name[0].family, + event.get("PERFORMING_PROFESSIONAL_SURNAME"), + ), + ( + "PERFORMING_PROFESSIONAL_FORENAME", + create_obj.contained[0].name[0].given[0], + event.get("PERFORMING_PROFESSIONAL_FORENAME"), + ), + ("LOCATION_CODE", create_obj.location.identifier.value, event.get("LOCATION_CODE")), + ("LOCATION_CODE_TYPE_URI", create_obj.location.identifier.system, event.get("LOCATION_CODE_TYPE_URI")), + ("SITE_CODE_TYPE_URI", create_obj.location.identifier.system, event.get("SITE_CODE_TYPE_URI")), + ("SITE_CODE", create_obj.performer[1].actor.identifier.value, event.get("SITE_CODE")), + ("INDICATION_CODE", create_obj.reasonCode[0].coding[0].code, event.get("INDICATION_CODE")), + ] + + for name, expected, actual in fields_to_compare: + check.is_true( + expected == actual, + f"Update ImmsID {context.ImmsID} with Version {context.expected_version} - Expected {name}: {expected}, Actual {actual}", + ) + + +def get_all_term_text(context): + item = fetch_immunization_int_delta_detail_by_immsID(context.aws_profile_name, context.ImmsID, context.S3_env) + assert item, f"Item not found in response for ImmsID: {context.ImmsID}" + + event = item[0].get("Imms") + assert event, "Imms field missing in items." + + assert "VACCINATION_PROCEDURE_TERM" in event, "Procedure term text field is missing in the delta table item." + procedure_term = event.get("VACCINATION_PROCEDURE_TERM") + + assert "VACCINE_PRODUCT_TERM" in event, "Product term text field is missing in the delta table item." + product_term = event.get("VACCINE_PRODUCT_TERM") + + assert "SITE_OF_VACCINATION_TERM" in event, "Site of vaccination term text field is missing in the delta table item." + site_term = event.get("SITE_OF_VACCINATION_TERM") + + assert "ROUTE_OF_VACCINATION_TERM" in event, ( + "Route of vaccination term text field is missing in the delta table item." + ) + route_term = event.get("ROUTE_OF_VACCINATION_TERM") + + return { + "procedure_term": procedure_term, + "product_term": product_term, + "site_term": site_term, + "route_term": route_term, + } + + +def get_all_the_vaccination_codes(list_items): + return [ + Coding(system=item["system"], code=item["code"], display=item["display"], extension=None) for item in list_items + ] + + +def validate_audit_table_record( + context, + item, + expected_status: str, + expected_error_detail: str = None, + expected_queue_name: str = None, + expected_record_count: str = None, +): + check.is_true( + item.get("status") == expected_status, f"Expected status {expected_status}, got '{item.get('status')}'" + ) + + expected_queue = expected_queue_name if expected_queue_name else f"{context.supplier_name}_{context.vaccine_type}" + check.is_true( + item.get("queue_name", "").upper() == expected_queue.upper(), + f"Expected queue_name '{expected_queue}', got '{item.get('queue_name')}'", + ) + + expected_row_count = len(context.vaccine_df) + + expected_success_count = context.vaccine_df[ + (~context.vaccine_df["UNIQUE_ID"].str.startswith("Fail-", na=False)) + & (context.vaccine_df["UNIQUE_ID"].str.strip() != "") + ].shape[0] + + expected_failure_count = context.vaccine_df[ + (context.vaccine_df["UNIQUE_ID"].str.startswith("Fail-", na=False)) + | (context.vaccine_df["UNIQUE_ID"].str.strip() == "") + ].shape[0] + + if expected_status == "Processed": + check.is_true( + item.get("record_count") == expected_row_count, + f"Expected record_count {expected_row_count}, got '{item.get('record_count')}'", + ) + + if expected_failure_count > 0: + check.is_true( + item.get("records_failed") == expected_failure_count, + f"Expected records_failed {expected_failure_count}, got '{item.get('records_failed')}'", + ) + + check.is_true( + item.get("records_succeeded") == expected_success_count, + f"Expected records_succeeded {expected_success_count}, got '{item.get('records_succeeded')}'", + ) + + check.is_true( + item.get("filename") == context.filename, f"Expected filename '{context.filename}', got '{item.get('filename')}'" + ) + + check.is_true("timestamp" in item, "processed_timestamp not found in item") + + check.is_true( + item.get("error_details") == (expected_error_detail if expected_error_detail != "None" else None), + f"Expected error_detail {expected_error_detail}, but got: {item.get('error_details')}", + ) + + +def validate_imms_delta_record_with_batch_record(context, batch_record, item, event_type, action_flag): + event = item.get("Imms") + assert event, "Imms field missing in items." + + fields_to_compare = [ + ("Operation", event_type.upper(), item.get("Operation")), + ("SupplierSystem", context.supplier_name.lower(), item.get("SupplierSystem").lower()), + ("VaccineType", f"{context.vaccine_type.lower()}", item.get("VaccineType").lower()), + ("Source", "IEDS", item.get("Source")), + ("CONVERSION_ERRORS", [], event.get("CONVERSION_ERRORS")), + ("PERSON_FORENAME", batch_record["PERSON_FORENAME"], event.get("PERSON_FORENAME")), + ("PERSON_SURNAME", batch_record["PERSON_SURNAME"], event.get("PERSON_SURNAME")), + ("NHS_NUMBER", batch_record["NHS_NUMBER"], event.get("NHS_NUMBER")), + ("PERSON_DOB", batch_record["PERSON_DOB"], event.get("PERSON_DOB")), + ("PERSON_POSTCODE", batch_record["PERSON_POSTCODE"], event.get("PERSON_POSTCODE")), + ( + "PERSON_GENDER_CODE", + get_gender_code(batch_record["PERSON_GENDER_CODE"]).value, + event.get("PERSON_GENDER_CODE"), + ), + ( + "VACCINATION_PROCEDURE_CODE", + batch_record["VACCINATION_PROCEDURE_CODE"], + event.get("VACCINATION_PROCEDURE_CODE"), + ), + ( + "VACCINATION_PROCEDURE_TERM", + batch_record["VACCINATION_PROCEDURE_TERM"], + event.get("VACCINATION_PROCEDURE_TERM"), + ), + ("VACCINE_PRODUCT_TERM", batch_record["VACCINE_PRODUCT_TERM"], event.get("VACCINE_PRODUCT_TERM")), + ("VACCINE_PRODUCT_CODE", batch_record["VACCINE_PRODUCT_CODE"], event.get("VACCINE_PRODUCT_CODE")), + ("VACCINE_MANUFACTURER", batch_record["VACCINE_MANUFACTURER"], event.get("VACCINE_MANUFACTURER")), + ("BATCH_NUMBER", batch_record["BATCH_NUMBER"], event.get("BATCH_NUMBER")), + ("RECORDED_DATE", batch_record["RECORDED_DATE"], event.get("RECORDED_DATE")), + ("EXPIRY_DATE", batch_record["EXPIRY_DATE"], event.get("EXPIRY_DATE")), + ("DOSE_SEQUENCE", batch_record["DOSE_SEQUENCE"], event.get("DOSE_SEQUENCE")), + ("DOSE_UNIT_TERM", batch_record["DOSE_UNIT_TERM"], event.get("DOSE_UNIT_TERM")), + ("DOSE_UNIT_CODE", batch_record["DOSE_UNIT_CODE"], event.get("DOSE_UNIT_CODE")), + ("SITE_OF_VACCINATION_TERM", batch_record["SITE_OF_VACCINATION_TERM"], event.get("SITE_OF_VACCINATION_TERM")), + ("SITE_OF_VACCINATION_CODE", batch_record["SITE_OF_VACCINATION_CODE"], event.get("SITE_OF_VACCINATION_CODE")), + ( + "DOSE_AMOUNT", + float(batch_record["DOSE_AMOUNT"]) if batch_record["DOSE_AMOUNT"] != "" else "", + float(event.get("DOSE_AMOUNT")) if event.get("DOSE_AMOUNT") != "" else "", + ), + ("PRIMARY_SOURCE", str(batch_record["PRIMARY_SOURCE"]).upper(), event.get("PRIMARY_SOURCE")), + ("ROUTE_OF_VACCINATION_TERM", batch_record["ROUTE_OF_VACCINATION_TERM"], event.get("ROUTE_OF_VACCINATION_TERM")), + ("ROUTE_OF_VACCINATION_CODE", batch_record["ROUTE_OF_VACCINATION_CODE"], event.get("ROUTE_OF_VACCINATION_CODE")), + ("ACTION_FLAG", action_flag, event.get("ACTION_FLAG")), + ("DATE_AND_TIME", batch_record["DATE_AND_TIME"], event.get("DATE_AND_TIME")), + ("UNIQUE_ID", batch_record["UNIQUE_ID"], event.get("UNIQUE_ID")), + ("UNIQUE_ID_URI", batch_record["UNIQUE_ID_URI"], event.get("UNIQUE_ID_URI")), + ( + "PERFORMING_PROFESSIONAL_SURNAME", + batch_record["PERFORMING_PROFESSIONAL_SURNAME"], + event.get("PERFORMING_PROFESSIONAL_SURNAME"), + ), + ( + "PERFORMING_PROFESSIONAL_FORENAME", + batch_record["PERFORMING_PROFESSIONAL_FORENAME"], + event.get("PERFORMING_PROFESSIONAL_FORENAME"), + ), + ("LOCATION_CODE", batch_record["LOCATION_CODE"], event.get("LOCATION_CODE")), + ("LOCATION_CODE_TYPE_URI", batch_record["LOCATION_CODE_TYPE_URI"], event.get("LOCATION_CODE_TYPE_URI")), + ("SITE_CODE_TYPE_URI", batch_record["SITE_CODE_TYPE_URI"], event.get("SITE_CODE_TYPE_URI")), + ("SITE_CODE", batch_record["SITE_CODE"], event.get("SITE_CODE")), + ("INDICATION_CODE", batch_record["INDICATION_CODE"], event.get("INDICATION_CODE")), + ] + + for name, expected, actual in fields_to_compare: + check.is_true( + expected == actual, + f"in Delta table - ImmsID {context.ImmsID} -- Expected {name}: {expected}, Actual {actual}", + ) + + +def validate_to_compare_batch_record_with_event_table_record(context, batch_record, created_event): + response_patient, response_practitioner = extract_patient_and_practitioner(created_event.contained) + + check.is_true(response_patient is not None, "Patient not found in contained resources") + if batch_record["PERFORMING_PROFESSIONAL_FORENAME"] or batch_record["PERFORMING_PROFESSIONAL_SURNAME"]: + check.is_true(response_practitioner is not None, "Practitioner not found in contained resources") + else: + check.is_true(response_practitioner is None, "Practitioner should not be present in contained resources") + + created_occurrence_date = batch_record["DATE_AND_TIME"] + trimmed_date = created_occurrence_date[:-2] + expected_occurrenceDateTime = f"{covert_to_expected_date_format(trimmed_date)}+00:00" + expected_recorded = covert_to_expected_date_format(batch_record["RECORDED_DATE"]) + actual_occurrenceDateTime = covert_to_expected_date_format(created_event.occurrenceDateTime) + actual_recorded = covert_to_expected_date_format(created_event.recorded) + gender_code = get_gender_code(batch_record["PERSON_GENDER_CODE"]) + expected_gender = GenderCode(gender_code).name.lower() + fields_to_compare = [] + + if batch_record["INDICATION_CODE"]: + fields_to_compare.extend( + [ + ("reasonCode.coding.code", batch_record["INDICATION_CODE"], created_event.reasonCode[0].coding[0].code), + ("reasonCode.coding.system", "http://snomed.info/sct", created_event.reasonCode[0].coding[0].system), + ] + ) + + if batch_record["NHS_NUMBER"]: + fields_to_compare.extend( + [ + ("Patient.identifier.value", batch_record["NHS_NUMBER"], response_patient.identifier[0].value), + ( + "Patient.identifier.system", + "https://fhir.nhs.uk/Id/nhs-number", + response_patient.identifier[0].system, + ), + ] + ) + + if batch_record["VACCINATION_PROCEDURE_TERM"]: + fields_to_compare.append( + ( + "extension.valueCodeableConcept.coding.extension.valueString", + batch_record["VACCINATION_PROCEDURE_TERM"], + created_event.extension[0].valueCodeableConcept.coding[0].display, + ) + ) + + if batch_record["SITE_OF_VACCINATION_CODE"]: + fields_to_compare.extend( + [ + ("site.coding.code", batch_record["SITE_OF_VACCINATION_CODE"], created_event.site.coding[0].code), + ("site.coding.system", "http://snomed.info/sct", created_event.site.coding[0].system), + ] + ) + + if batch_record["SITE_OF_VACCINATION_TERM"]: + fields_to_compare.extend( + [ + ("site.coding.system", "http://snomed.info/sct", created_event.site.coding[0].system), + ( + "site.coding.extension.display", + batch_record["SITE_OF_VACCINATION_TERM"], + created_event.site.coding[0].display, + ), + ] + ) + + if batch_record["VACCINE_PRODUCT_TERM"]: + fields_to_compare.extend( + [ + ("vaccineCode.coding.system", "http://snomed.info/sct", created_event.vaccineCode.coding[0].system), + ( + "vaccineCode.coding.extension.valueString", + batch_record["VACCINE_PRODUCT_TERM"], + created_event.vaccineCode.coding[0].display, + ), + ] + ) + + if batch_record["VACCINE_PRODUCT_CODE"]: + fields_to_compare.extend( + [ + ( + "vaccineCode.coding.code", + batch_record["VACCINE_PRODUCT_CODE"], + created_event.vaccineCode.coding[0].code, + ), + ("vaccineCode.coding.system", "http://snomed.info/sct", created_event.vaccineCode.coding[0].system), + ] + ) + + if batch_record["ROUTE_OF_VACCINATION_CODE"]: + fields_to_compare.extend( + [ + ("route.coding.code", batch_record["ROUTE_OF_VACCINATION_CODE"], created_event.route.coding[0].code), + ("route.coding.system", "http://snomed.info/sct", created_event.route.coding[0].system), + ] + ) + + if batch_record["ROUTE_OF_VACCINATION_TERM"]: + fields_to_compare.extend( + [ + ("route.coding.system", "http://snomed.info/sct", created_event.route.coding[0].system), + ( + "route.coding.display", + batch_record["ROUTE_OF_VACCINATION_TERM"], + created_event.route.coding[0].display, + ), + ] + ) + + if batch_record["VACCINE_MANUFACTURER"]: + fields_to_compare.append( + ("manufacturer", batch_record["VACCINE_MANUFACTURER"], created_event.manufacturer["display"]) + ) + + if batch_record["BATCH_NUMBER"]: + fields_to_compare.append(("lotNumber", batch_record["BATCH_NUMBER"], created_event.lotNumber)) + + if batch_record["EXPIRY_DATE"]: + fields_to_compare.append( + ("expirationDate", format_date_yyyymmdd(batch_record["EXPIRY_DATE"]), created_event.expirationDate) + ) + + if batch_record["DOSE_AMOUNT"]: + fields_to_compare.append( + ("doseQuantity.value", float(batch_record["DOSE_AMOUNT"]), created_event.doseQuantity.value) + ) + + if batch_record["DOSE_UNIT_TERM"]: + fields_to_compare.extend( + [ + ("doseQuantity.term", batch_record["DOSE_UNIT_TERM"], created_event.doseQuantity.unit), + ] + ) + + if batch_record["DOSE_UNIT_CODE"]: + fields_to_compare.extend( + [ + ("doseQuantity.code", batch_record["DOSE_UNIT_CODE"], created_event.doseQuantity.code), + ("doseQuantity.system", "http://snomed.info/sct", created_event.doseQuantity.system), + ] + ) + + if batch_record["DOSE_SEQUENCE"]: + fields_to_compare.append( + ("protocolApplied.doseNumberPositiveInt", 1, created_event.protocolApplied[0].doseNumberPositiveInt) + ) + else: + fields_to_compare.append( + ( + "protocolApplied.doseNumberNotProvided", + "Dose sequence not recorded", + created_event.protocolApplied[0].doseNumberString, + ) + ) + + if batch_record["PERFORMING_PROFESSIONAL_FORENAME"] or batch_record["PERFORMING_PROFESSIONAL_SURNAME"]: + p_names = extract_practitioner_name(response_practitioner) + fields_to_compare.extend( + [ + ("Practitioner.id", "Practitioner1", response_practitioner.id), + ("performer.actor.reference", "#Practitioner1", created_event.performer[1].actor.reference), + ] + ) + + if batch_record["PERFORMING_PROFESSIONAL_FORENAME"]: + fields_to_compare.append( + ( + "Practitioner.name.family", + batch_record["PERFORMING_PROFESSIONAL_SURNAME"], + p_names["Practitioner.name.family"], + ) + ) + + if batch_record["PERFORMING_PROFESSIONAL_SURNAME"]: + fields_to_compare.append( + ( + "Practitioner.name.family", + batch_record["PERFORMING_PROFESSIONAL_SURNAME"], + p_names["Practitioner.name.family"], + ) + ) + + fields_to_compare.extend( + [ + ("patient.reference", "#Patient1", created_event.patient.reference), + ("Id", context.ImmsID, created_event.id), + ("resourceType", "Immunization", created_event.resourceType), + ("identifier.system", batch_record["UNIQUE_ID_URI"], created_event.identifier[0].system), + ("identifier.value", batch_record["UNIQUE_ID"], created_event.identifier[0].value), + ("status", "completed", created_event.status), + ("occurrenceDateTime", expected_occurrenceDateTime, actual_occurrenceDateTime), + ("Recorded", expected_recorded, actual_recorded), + ("primarySource", str(batch_record["PRIMARY_SOURCE"]).lower(), str(created_event.primarySource).lower()), + ("location.value", batch_record["LOCATION_CODE"], created_event.location.identifier.value), + ("location.system", batch_record["LOCATION_CODE_TYPE_URI"], created_event.location.identifier.system), + ( + "protocolApplied", + True, + compare_protocol_codings_to_reference( + created_event.protocolApplied, PROTOCOL_DISEASE_MAP.get(context.vaccine_type.upper(), []) + ), + ), + ("Patient.id", "Patient1", response_patient.id), + ("Patient.birthdate", format_date_yyyymmdd(batch_record["PERSON_DOB"]), response_patient.birthDate), + ("Patient.Gender", expected_gender, response_patient.gender), + ("Patient.name.family", batch_record["PERSON_SURNAME"], response_patient.name[0].family), + ("Patient.name.given", batch_record["PERSON_FORENAME"], response_patient.name[0].given[0]), + ("Patient.address.postalCode", batch_record["PERSON_POSTCODE"], response_patient.address[0].postalCode), + ( + "extension.url", + "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + created_event.extension[0].url, + ), + ( + "extension.valueCodeableConcept.coding.code", + batch_record["VACCINATION_PROCEDURE_CODE"], + created_event.extension[0].valueCodeableConcept.coding[0].code, + ), + ( + "extension.valueCodeableConcept.coding.system", + "http://snomed.info/sct", + created_event.extension[0].valueCodeableConcept.coding[0].system, + ), + ("performer.actor.type", "Organization", created_event.performer[0].actor.type), + ( + "performer.actor.identifier.value", + batch_record["SITE_CODE"], + created_event.performer[0].actor.identifier.value, + ), + ( + "performer.actor.identifier.system", + batch_record["SITE_CODE_TYPE_URI"], + created_event.performer[0].actor.identifier.system, + ), + ] + ) + + for name, expected, actual in fields_to_compare: + check.is_true(expected == actual, f"Event table Expected {name}: {expected}, Actual {actual}") + + +def normalize_coding(coding) -> Dict[str, str]: + """Extracts and normalizes a coding dict from a Coding object.""" + return {"system": coding.system, "code": coding.code, "display": coding.display} + + +def extract_protocol_codings(protocol_applied) -> List[Dict[str, str]]: + """Flattens all codings from protocolApplied into a list of normalized dicts.""" + codings = [] + for protocol in protocol_applied: + for disease in protocol.targetDisease: + for coding in disease.coding: + codings.append(normalize_coding(coding)) + return codings + + +def compare_protocol_codings_to_reference( + protocol_applied: List[ProtocolApplied], reference_codings: List[Dict[str, str]] +) -> bool: + extracted = extract_protocol_codings(protocol_applied) + + # Convert both lists to Counter of sorted tuples for order-insensitive comparison + extracted_counter = Counter(tuple(sorted(d.items())) for d in extracted) + reference_counter = Counter(tuple(sorted(d.items())) for d in reference_codings) + + return extracted_counter == reference_counter + + +def extract_patient_and_practitioner(contained): + patient = None + practitioner = None + + for resource in contained: + if resource.resourceType == "Patient": + patient = resource + elif resource.resourceType == "Practitioner": + practitioner = resource + + return patient, practitioner + + +def get_gender_code(input: str) -> GenderCode: + normalized = input.strip().lower() + try: + return GenderCode[normalized] + except KeyError: + pass + + for gender in GenderCode: + if gender.value == normalized: + return gender + + raise ValueError(f"Invalid gender input: {input}") + + +def update_audit_table_for_failed_status(item: dict, aws_profile_name: str, env: str): + if item.get("status") != "Failed": + return + + db = DynamoDBHelper(aws_profile_name, env) + table = db.get_batch_audit_table() + + key = {"message_id": item["message_id"]} + + response = table.update_item( + Key=key, + UpdateExpression="SET #s = :new_status", + ExpressionAttributeNames={"#s": "status"}, + ExpressionAttributeValues={":new_status": "Not processed - Automation testing"}, + ReturnValues="UPDATED_NEW", + ) + + print(f"✅ Updated audit status for message_id={key['message_id']}: {response.get('Attributes')}") diff --git a/tests/e2e_automation/src/objectModels/__init__.py b/tests/e2e_automation/src/objectModels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_automation/src/objectModels/api_data_objects.py b/tests/e2e_automation/src/objectModels/api_data_objects.py new file mode 100644 index 0000000000..a3bac1d981 --- /dev/null +++ b/tests/e2e_automation/src/objectModels/api_data_objects.py @@ -0,0 +1,263 @@ +from typing import Any, Dict, List, Literal, Optional, Union + +from pydantic import BaseModel, Field +from typing_extensions import Annotated + + +class ExtensionItem(BaseModel): + url: str + valueString: Optional[str] = None + valueId: Optional[str] = None + + +class Coding(BaseModel): + extension: Optional[List[ExtensionItem]] = None + system: str + code: Optional[str] = None + display: Optional[str] = None + + +class CodeableConcept(BaseModel): + coding: Optional[List[Coding]] = None + text: Optional[str] = None + + +class Period(BaseModel): + start: str + end: str + + +class Identifier(BaseModel): + system: Optional[str] = None + value: Optional[str] = None + use: Optional[str] = None + type: Optional[CodeableConcept] = None + period: Optional[Period] = None + + +class Reference(BaseModel): + reference: Optional[str] = None + type: Optional[str] = None + identifier: Optional[Identifier] = None + + +class HumanName(BaseModel): + family: Optional[str] = None + given: Optional[List[str]] = None + + +class Address(BaseModel): + use: Optional[str] = None + type: Optional[str] = None + text: Optional[str] = None + line: Optional[List[str]] = None + city: Optional[str] = None + district: Optional[str] = None + state: Optional[str] = None + postalCode: str + country: Optional[str] = None + period: Optional[Period] = None + + +class Practitioner(BaseModel): + resourceType: str = "Practitioner" + id: str + name: List[HumanName] + + +class Patient(BaseModel): + resourceType: str = "Patient" + id: str + identifier: Optional[List[Identifier]] = None + name: List[HumanName] + gender: str + birthDate: str + address: List[Address] + + +class Extension(BaseModel): + url: str + valueCodeableConcept: CodeableConcept + + +class Performer(BaseModel): + actor: Reference # Updated to match FHIR structure + + +class ReasonCode(BaseModel): + coding: List[Coding] + text: Optional[str] = None + + +class DoseQuantity(BaseModel): + value: Optional[float] = None + unit: Optional[str] = None + system: Optional[str] = None + code: Optional[str] = None + + +class ProtocolApplied(BaseModel): + targetDisease: List[CodeableConcept] + doseNumberPositiveInt: Optional[int] = None + doseNumberString: Optional[str] = None + + +class LocationIdentifier(BaseModel): + system: str + value: str + + +class Location(BaseModel): + identifier: LocationIdentifier + + +class Immunization(BaseModel): + resourceType: str = "Immunization" + contained: List[Any] + extension: List[Extension] + identifier: List[Identifier] + status: str = "completed" + vaccineCode: CodeableConcept # Fixed type + patient: Reference # Fixed type + manufacturer: Dict[str, str] + location: Location + site: CodeableConcept + route: CodeableConcept + doseQuantity: DoseQuantity + performer: List[Performer] + reasonCode: List[ReasonCode] + protocolApplied: List[ProtocolApplied] + occurrenceDateTime: str = "" + recorded: str = "" + primarySource: bool = True + lotNumber: str = "" + expirationDate: str = "" + + class Config: + orm_mode = True + + +class ResponseActorOrganization(BaseModel): + type: str = "Organization" + display: Optional[str] = None + identifier: Optional[Identifier] + + +class ResponsePerformer(BaseModel): + actor: ResponseActorOrganization + + +class Link(BaseModel): + relation: str + url: str + + +class Search(BaseModel): + mode: str + + +class PatientIdentifier(BaseModel): + system: str + value: Optional[str] = None + + +class ResponsePatient(BaseModel): + reference: str + type: Optional[str] = None + identifier: Optional[PatientIdentifier] = None + + +class Meta(BaseModel): + versionId: str + + +class ImmunizationResponse(BaseModel): + resourceType: Literal["Immunization"] + id: str + meta: Meta + extension: List[Extension] + identifier: List[Identifier] + status: str + vaccineCode: CodeableConcept + patient: ResponsePatient + occurrenceDateTime: str + recorded: str + lotNumber: str + expirationDate: str + primarySource: bool + location: Location + manufacturer: Dict[str, Any] + site: CodeableConcept + route: CodeableConcept + doseQuantity: DoseQuantity + performer: Optional[List[ResponsePerformer]] + reasonCode: List[ReasonCode] + protocolApplied: List[ProtocolApplied] + + +class ImmunizationUpdate(BaseModel): + resourceType: Literal["Immunization"] + id: str + contained: List[Union[Patient, Practitioner]] + extension: List[Extension] + identifier: List[Identifier] + status: str = "completed" + vaccineCode: CodeableConcept # Fixed type + patient: Reference # Fixed type + manufacturer: Dict[str, str] + location: Location + site: CodeableConcept + route: CodeableConcept + doseQuantity: DoseQuantity + performer: List[Performer] + reasonCode: List[ReasonCode] + protocolApplied: List[ProtocolApplied] + occurrenceDateTime: str = "" + recorded: str = "" + primarySource: bool = True + lotNumber: str = "" + expirationDate: str = "" + + +class PatientResource(BaseModel): + resourceType: Literal["Patient"] + id: str + identifier: List[PatientIdentifier] + + +class Entry(BaseModel): + fullUrl: str + resource: Annotated[Union[ImmunizationResponse, PatientResource], Field(discriminator="resourceType")] + search: Dict[str, str] + + +class FHIRImmunizationResponse(BaseModel): + resourceType: str + type: Optional[str] = None + link: Optional[List[Link]] = [] + entry: Optional[List[Entry]] = [] + total: Optional[int] = None + + +class ImmunizationReadResponse_IntTable(BaseModel): + resourceType: str + contained: List[Union[Patient, Practitioner]] + extension: List[Extension] + identifier: List[Identifier] + status: str + vaccineCode: CodeableConcept + patient: Reference + manufacturer: Optional[Dict[str, str]] = None + id: str + location: Location + site: Optional[CodeableConcept] = None + route: Optional[CodeableConcept] = None + doseQuantity: Optional[DoseQuantity] = None + performer: List[Performer] + reasonCode: Optional[List[ReasonCode]] = None + protocolApplied: List[ProtocolApplied] + occurrenceDateTime: str = "" + recorded: str = "" + primarySource: bool = True + lotNumber: str = "" + expirationDate: str = "" diff --git a/tests/e2e_automation/src/objectModels/api_immunization_builder.py b/tests/e2e_automation/src/objectModels/api_immunization_builder.py new file mode 100644 index 0000000000..69bee840ff --- /dev/null +++ b/tests/e2e_automation/src/objectModels/api_immunization_builder.py @@ -0,0 +1,257 @@ +import random +import uuid +from datetime import datetime, timedelta +from typing import Any, Dict, List + +from utilities.date_helper import generate_date +from utilities.vaccination_constants import ( + DOSE_QUANTITY_MAP, + PROTOCOL_DISEASE_MAP, + REASON_CODE_MAP, + ROUTE_MAP, + SITE_MAP, + VACCINATION_PROCEDURE_MAP, + VACCINE_CODE_MAP, +) + +from src.objectModels.api_data_objects import ( + CodeableConcept, + Coding, + DoseQuantity, + Extension, + ExtensionItem, + HumanName, + Identifier, + Immunization, + ImmunizationUpdate, + Location, + LocationIdentifier, + Performer, + Period, + Practitioner, + ProtocolApplied, + Reference, +) + + +def create_extension(url: str, stringValue: str = None, idValue: str = None) -> ExtensionItem: + return ExtensionItem(url=url, valueString=stringValue, valueId=idValue) + + +def build_vaccine_procedure_code(vaccine_type: str, text: str = None, add_extensions: bool = True) -> CodeableConcept: + try: + selected_vaccine_procedure = random.choice(VACCINATION_PROCEDURE_MAP[vaccine_type.upper()]) + except KeyError: + raise ValueError(f"Unsupported vaccine type: {vaccine_type}") + + extensions = None + if add_extensions: + extensions = [ + create_extension( + "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + stringValue=selected_vaccine_procedure["stringValue"], + ), + create_extension( + "http://hl7.org/fhir/StructureDefinition/coding-sctdescid", idValue=selected_vaccine_procedure["idValue"] + ), + ] + + return CodeableConcept( + coding=[ + Coding( + system=selected_vaccine_procedure["system"], + code=selected_vaccine_procedure["code"], + display=selected_vaccine_procedure["display"], + extension=extensions, + ) + ], + text=text, + ) + + +def build_vaccine_procedure_extension(vaccine_type: str, text: str = None) -> Extension: # type: ignore + return Extension( + url="https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure", + valueCodeableConcept=build_vaccine_procedure_code(vaccine_type, text), # type: ignore + ) + + +def build_location_identifier() -> Location: + return Location(identifier=LocationIdentifier(system="https://fhir.nhs.uk/Id/ods-organization-code", value="X99999")) + + +def get_vaccine_details( + vaccine_type: str, vacc_text: str = None, lot_number: str = "", expiry_date: str = "", add_extensions: bool = True +) -> Dict[str, Any]: + selected_vaccine = random.choice(VACCINE_CODE_MAP[vaccine_type.upper()]) + + extensions = None + if add_extensions: + extensions = [ + create_extension( + "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + stringValue=selected_vaccine["stringValue"], + ), + create_extension( + "http://hl7.org/fhir/StructureDefinition/coding-sctdescid", idValue=selected_vaccine["idValue"] + ), + ] + + vaccine_code = CodeableConcept( + coding=[ + Coding( + system=selected_vaccine["system"], + code=selected_vaccine["code"], + display=selected_vaccine["display"], + extension=extensions, + ) + ], + text=vacc_text, + ) + + manufacturer = {"display": selected_vaccine["manufacturer"]} + + if not lot_number: + lot_number = str(random.randint(100000, 999999)) + + if not expiry_date: + future_date = datetime.now() + timedelta(days=365 * 2) + expiry_date = future_date.strftime("%Y-%m-%d") + + return { + "vaccine_code": vaccine_code, + "manufacturer": manufacturer, + "lotNumber": lot_number, + "expiryDate": expiry_date, + } + + +def build_performer() -> List[Performer]: + return [ + Performer(actor=Reference(reference="#Pract1", type="Practitioner")), + Performer( + actor=Reference( + reference="Organization/B0C4P", + type="Organization", + identifier=Identifier( + value="B0C4P", + system="https://fhir.nhs.uk/Id/ods-organization-code", + use="usual", + type=CodeableConcept( + coding=[ + Coding( + system="http://terminology.hl7.org/CodeSystem/v2-0203", + code="123456", + display="Test display performer", + version="Test version performer", + userSelected=True, + ) + ], + text="test string performer", + ), + period=Period(start="2000-01-01", end="2025-01-01"), + ), + display="UNIVERSITY HOSPITAL OF WALES", + ) + ), + ] + + +def remove_empty_fields(data): + """Recursively removes fields with empty values from a dictionary.""" + if isinstance(data, dict): + return {k: remove_empty_fields(v) for k, v in data.items() if v != ""} + elif isinstance(data, list): + return [remove_empty_fields(item) for item in data] + else: + return data + + +def build_site_route(obj: Coding, text: str = None, add_extensions: bool = True) -> CodeableConcept: + extensions = None + if add_extensions: + extensions = [ + create_extension( + "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-CodingSCTDescDisplay", + stringValue=obj["stringValue"], + ), + create_extension("http://hl7.org/fhir/StructureDefinition/coding-sctdescid", idValue=obj["idValue"]), + ] + + return CodeableConcept( + coding=[Coding(system=obj["system"], code=obj["code"], display=obj["display"], extension=extensions)], text=text + ) + + +def build_protocol_applied(vaccine_type: str, doseNumber: int = 1) -> ProtocolApplied: + list_of_diseases = PROTOCOL_DISEASE_MAP.get(vaccine_type.upper(), []) + return ProtocolApplied( + targetDisease=[ + CodeableConcept( + coding=[ + Coding(system=disease["system"], code=disease["code"], display=disease["display"], extension=None) + ] + ) + for disease in list_of_diseases + ], + doseNumberPositiveInt=doseNumber, + ) + + +def create_immunization_object(patient, vaccine_type: str) -> Immunization: + practitioner = Practitioner( + resourceType="Practitioner", # ✅ Explicitly set resourceType + id="Pract1", + name=[HumanName(family="Furlong", given=["Darren"])], + ) + extension = [build_vaccine_procedure_extension(vaccine_type.upper())] + vaccine_details = get_vaccine_details(vaccine_type) + + return Immunization( + resourceType="Immunization", + contained=[practitioner, patient], + extension=extension, + identifier=[Identifier(system="https://supplierABC/identifiers/vacc", value=str(uuid.uuid4()))], + vaccineCode=vaccine_details["vaccine_code"], + patient=Reference(reference=f"#{patient.id}", type="Patient"), + occurrenceDateTime=generate_date("current_occurrence"), + recorded=generate_date("current_occurrence"), + manufacturer=vaccine_details["manufacturer"], + location=build_location_identifier(), + lotNumber=vaccine_details["lotNumber"], + status="completed", + primarySource=True, + expirationDate=vaccine_details["expiryDate"], + site=build_site_route(random.choice(SITE_MAP)), + route=build_site_route(random.choice(ROUTE_MAP)), + doseQuantity=DoseQuantity(**random.choice(DOSE_QUANTITY_MAP)), + performer=build_performer(), + reasonCode=[CodeableConcept(coding=[random.choice(REASON_CODE_MAP)])], + protocolApplied=[build_protocol_applied(vaccine_type.upper())], + ) + + +def convert_to_update(immunization: Immunization, id: str) -> ImmunizationUpdate: + return ImmunizationUpdate( + resourceType=immunization.resourceType, + id=id, + contained=immunization.contained, + extension=immunization.extension, + identifier=immunization.identifier, + status=immunization.status, + vaccineCode=immunization.vaccineCode, + patient=immunization.patient, + occurrenceDateTime=immunization.occurrenceDateTime, + recorded=immunization.recorded, + lotNumber=immunization.lotNumber, + expirationDate=immunization.expirationDate, + primarySource=immunization.primarySource, + location=immunization.location, + manufacturer=immunization.manufacturer, + site=immunization.site, + route=immunization.route, + doseQuantity=immunization.doseQuantity, + performer=immunization.performer, + reasonCode=immunization.reasonCode, + protocolApplied=immunization.protocolApplied, + ) diff --git a/tests/e2e_automation/src/objectModels/api_operation_outcome.py b/tests/e2e_automation/src/objectModels/api_operation_outcome.py new file mode 100644 index 0000000000..db2993c70a --- /dev/null +++ b/tests/e2e_automation/src/objectModels/api_operation_outcome.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +from pydantic import BaseModel + + +class Coding(BaseModel): + system: Optional[str] = None + code: Optional[str] = None + + +class Details(BaseModel): + coding: List[Coding] + + +class Issue(BaseModel): + severity: Optional[str] = None + code: Optional[str] = None + details: Optional[Details] = None + diagnostics: Optional[str] = None + + +class Meta(BaseModel): + profile: List[str] + + +class OperationOutcome(BaseModel): + resourceType: str + id: Optional[str] = None + meta: Optional[Meta] = None + issue: List[Issue] diff --git a/tests/e2e_automation/src/objectModels/api_search_object.py b/tests/e2e_automation/src/objectModels/api_search_object.py new file mode 100644 index 0000000000..f75c4a2c99 --- /dev/null +++ b/tests/e2e_automation/src/objectModels/api_search_object.py @@ -0,0 +1,46 @@ +from dataclasses import asdict, dataclass +from urllib.parse import urlencode + + +@dataclass +class ImmunizationRequest: + patient_identifier: str + immunization_target: str + include: str + date_from: str + date_to: str + + +def set_request_data( + nhs_number, target, date_from: str = None, date_to: str = None, include: str = "Immunization:patient" +) -> ImmunizationRequest: + return ImmunizationRequest( + patient_identifier=f"https://fhir.nhs.uk/Id/nhs-number|{nhs_number}", + immunization_target=target, + include=include, + date_from=date_from, + date_to=date_to, + ) + + +def convert_to_form_data(request: ImmunizationRequest) -> str: + data_dict = asdict(request) + + field_mapping = { + "patient_identifier": "patient.identifier", + "immunization_target": "-immunization.target", + "include": "_include", + "date_from": "-date.from", + "date_to": "-date.to", + } + + clean_data = {field_mapping[key]: value for key, value in data_dict.items() if value is not None} + + include_value = clean_data.pop("_include", None) + + encoded_data = urlencode(clean_data, safe="|") + + if include_value: + encoded_data += f"&_include={include_value}" + + return encoded_data diff --git a/tests/e2e_automation/src/objectModels/batch/batch_data_object.py b/tests/e2e_automation/src/objectModels/batch/batch_data_object.py new file mode 100644 index 0000000000..50cea26871 --- /dev/null +++ b/tests/e2e_automation/src/objectModels/batch/batch_data_object.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel + + +class BatchVaccinationRecord(BaseModel): + NHS_NUMBER: str + PERSON_FORENAME: str + PERSON_SURNAME: str + PERSON_DOB: str + PERSON_GENDER_CODE: str + PERSON_POSTCODE: str + DATE_AND_TIME: str + SITE_CODE: str + SITE_CODE_TYPE_URI: str + UNIQUE_ID: str + UNIQUE_ID_URI: str + ACTION_FLAG: str + PERFORMING_PROFESSIONAL_FORENAME: str + PERFORMING_PROFESSIONAL_SURNAME: str + RECORDED_DATE: str + PRIMARY_SOURCE: str + VACCINATION_PROCEDURE_CODE: str + VACCINATION_PROCEDURE_TERM: str + DOSE_SEQUENCE: str + VACCINE_PRODUCT_CODE: str + VACCINE_PRODUCT_TERM: str + VACCINE_MANUFACTURER: str + BATCH_NUMBER: str + EXPIRY_DATE: str + SITE_OF_VACCINATION_CODE: str + SITE_OF_VACCINATION_TERM: str + ROUTE_OF_VACCINATION_CODE: str + ROUTE_OF_VACCINATION_TERM: str + DOSE_AMOUNT: str + DOSE_UNIT_CODE: str + DOSE_UNIT_TERM: str + INDICATION_CODE: str + LOCATION_CODE: str + LOCATION_CODE_TYPE_URI: str diff --git a/tests/e2e_automation/src/objectModels/batch/batch_file_builder.py b/tests/e2e_automation/src/objectModels/batch/batch_file_builder.py new file mode 100644 index 0000000000..99b4e95e0c --- /dev/null +++ b/tests/e2e_automation/src/objectModels/batch/batch_file_builder.py @@ -0,0 +1,153 @@ +import csv +import random +import re +import uuid +from typing import Any, Dict, Optional + +from src.objectModels.batch.batch_data_object import BatchVaccinationRecord +from src.objectModels.patient_loader import load_patient_by_id +from utilities.date_helper import generate_date +from utilities.enums import GenderCode +from utilities.vaccination_constants import ROUTE_MAP, SITE_MAP, VACCINATION_PROCEDURE_MAP, VACCINE_CODE_MAP + + +def build_procedure_code(vaccine_type: str) -> Dict[str, str]: + try: + selected = random.choice(VACCINATION_PROCEDURE_MAP[vaccine_type.upper()]) + return {"term": selected["display"], "code": selected["code"]} + except KeyError: + raise ValueError(f"Unsupported vaccine type: {vaccine_type}") + + +def build_vaccine_details(vaccine_type: str, lot_number: str = "", expiry_date: str = "") -> Dict[str, Any]: + try: + selected = random.choice(VACCINE_CODE_MAP[vaccine_type.upper()]) + except KeyError: + raise ValueError(f"Unsupported vaccine type: {vaccine_type}") + + return { + "term": selected["display"], + "code": selected["code"], + "manufacturer": selected["manufacturer"], + "lot_number": lot_number or str(random.randint(100000, 999999)), + "expiry_date": expiry_date or generate_date("current_date").replace("-", ""), + } + + +def build_location_site_identifier(value: str = "X99999") -> Dict[str, str]: + return {"system": "https://fhir.nhs.uk/Id/ods-organization-code", "value": value} + + +def get_batch_date(date_str: str = "current_occurrence") -> str: + raw_date = generate_date(date_str) + cleaned_date = re.sub(r"[^\w]", "", raw_date) + return cleaned_date + + +def get_performing_professional(forename: str = "Automation", surname: str = "Tests") -> Dict[str, str]: + return {"performing_professional_forename": forename, "performing_professional_surname": surname} + + +def build_site_of_vaccination() -> Dict[str, str]: + selected = random.choice(SITE_MAP) + return {"site_of_vaccination_code": selected["code"], "site_of_vaccination_term": selected["display"]} + + +def build_route_of_vaccination() -> Dict[str, str]: + selected = random.choice(ROUTE_MAP) + return {"route_of_vaccination_code": selected["code"], "route_of_vaccination_term": selected["display"]} + + +def build_dose_details( + dose_sequence: str = "1", dose_amount: str = "0.5", dose_unit_code: str = "ml", dose_unit_term: str = "millilitre" +) -> Dict[str, str]: + return { + "dose_sequence": dose_sequence, + "dose_amount": dose_amount, + "dose_unit_code": dose_unit_code, + "dose_unit_term": dose_unit_term, + } + + +def build_unique_reference(unique_id: Optional[str] = None) -> Dict[str, str]: + uid = unique_id or str(uuid.uuid4()) + return {"unique_id": uid, "unique_id_uri": "https://fhir.nhs.uk/Id/Automation-vaccine-administered-event-uk"} + + +def get_patient_details(context) -> Dict[str, str]: + patient = load_patient_by_id(context.patient_id) + return { + "first_name": patient.name[0].given[0], + "last_name": patient.name[0].family, + "nhs_number": patient.identifier[0].value, + "gender": GenderCode[patient.gender].value, + "birth_date": patient.birthDate.replace("-", ""), + "postal_code": patient.address[0].postalCode, + } + + +def generate_file_name(context) -> str: + return f"{context.vaccine_type}_Vaccinations_v5_{context.supplier_ods_code}_{context.FileTimestamp}.{context.file_extension}" + + +def build_batch_file(context, unique_id: str = None) -> BatchVaccinationRecord: + patient = get_patient_details(context) + location = build_location_site_identifier() + procedure = build_procedure_code(context.vaccine_type) + vaccine = build_vaccine_details(context.vaccine_type) + professional = get_performing_professional() + site = build_site_of_vaccination() + route = build_route_of_vaccination() + dose = build_dose_details() + unique = build_unique_reference(unique_id) + + return BatchVaccinationRecord( + NHS_NUMBER=patient["nhs_number"], + PERSON_FORENAME=patient["first_name"], + PERSON_SURNAME=patient["last_name"], + PERSON_DOB=patient["birth_date"], + PERSON_GENDER_CODE=patient["gender"], + PERSON_POSTCODE=patient["postal_code"], + DATE_AND_TIME=get_batch_date("current_occurrence_with_milliseconds"), + SITE_CODE=location["value"], + SITE_CODE_TYPE_URI=location["system"], + UNIQUE_ID=unique["unique_id"], + UNIQUE_ID_URI=unique["unique_id_uri"], + ACTION_FLAG="NEW", + PERFORMING_PROFESSIONAL_FORENAME=professional["performing_professional_forename"], + PERFORMING_PROFESSIONAL_SURNAME=professional["performing_professional_surname"], + RECORDED_DATE=get_batch_date("current_date"), + PRIMARY_SOURCE="TRUE", + VACCINATION_PROCEDURE_CODE=procedure["code"], + VACCINATION_PROCEDURE_TERM=procedure["term"], + DOSE_SEQUENCE=dose["dose_sequence"], + VACCINE_PRODUCT_CODE=vaccine["code"], + VACCINE_PRODUCT_TERM=vaccine["term"], + VACCINE_MANUFACTURER=vaccine["manufacturer"], + BATCH_NUMBER=vaccine["lot_number"], + EXPIRY_DATE=vaccine["expiry_date"], + SITE_OF_VACCINATION_CODE=site["site_of_vaccination_code"], + SITE_OF_VACCINATION_TERM=site["site_of_vaccination_term"], + ROUTE_OF_VACCINATION_CODE=route["route_of_vaccination_code"], + ROUTE_OF_VACCINATION_TERM=route["route_of_vaccination_term"], + DOSE_AMOUNT=dose["dose_amount"], + DOSE_UNIT_CODE=dose["dose_unit_code"], + DOSE_UNIT_TERM=dose["dose_unit_term"], + INDICATION_CODE="443684005", + LOCATION_CODE=location["value"], + LOCATION_CODE_TYPE_URI=location["system"], + ) + + +def save_record_to_batch_files_directory(context, delimiter): + file_path = f"{context.working_directory}/{context.filename}" + df = context.vaccine_df.copy() + df.reset_index(drop=True, inplace=True) + + with open(file_path, mode="w", newline="", encoding="utf-8") as file: + writer = csv.writer(file, delimiter=delimiter, quoting=csv.QUOTE_ALL) + writer.writerow(df.columns.tolist()) + for row in df.itertuples(index=False): + writer.writerow(row) + + print(f"✅ Pipe-delimited file saved to: {file_path}") diff --git a/tests/e2e_automation/src/objectModels/patient_loader.py b/tests/e2e_automation/src/objectModels/patient_loader.py new file mode 100644 index 0000000000..dddf0cc136 --- /dev/null +++ b/tests/e2e_automation/src/objectModels/patient_loader.py @@ -0,0 +1,53 @@ +import pandas as pd + +from src.objectModels.api_data_objects import Address, HumanName, Identifier, Patient + +csv_path = "input/testData.csv" + + +def load_patient_by_id(id: str) -> Patient: + row = read_patient_from_csv(id) # FIXED: Correct function call + + if row is None: + raise ValueError(f"NHS number {id} not found in {csv_path}") + + nhs_number = row.get("nhs_number", "").strip() + nhs_number = None if not nhs_number or nhs_number.lower() in ["null", "none"] else nhs_number + + identifier = Identifier(system="https://fhir.nhs.uk/Id/nhs-number", value=nhs_number) + + name = HumanName(family=row["family_name"], given=[row["given_name"]]) + + address = Address( + use="Home", + type="Postal", + text="Validate Obf", + line=[row["address_line"]], + city=row["city"], + district=row["district"], + state=row["state"], + postalCode=row["postal_code"], + country=row["country"], + period={"start": row["start_date"], "end": row["end_date"]}, + ) + + return Patient( + id="Pat1", + resourceType="Patient", + identifier=[identifier], + name=[name], + gender=row["gender"], + birthDate=row["birth_date"], + address=[address], + ) + + +def read_patient_from_csv(id: str): + df = pd.read_csv(csv_path, dtype=str) + + valid_patients = df[df["id"] == id] if id != "Random" else df[df["id"] == "Valid_NHS"] + + if not valid_patients.empty: + return valid_patients.sample(1).to_dict(orient="records")[0] + + return None # FIXED: Return None instead of raising an exception diff --git a/tests/e2e_automation/utilities/__init__.py b/tests/e2e_automation/utilities/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_automation/utilities/api_fhir_immunization_helper.py b/tests/e2e_automation/utilities/api_fhir_immunization_helper.py new file mode 100644 index 0000000000..69c5f182dd --- /dev/null +++ b/tests/e2e_automation/utilities/api_fhir_immunization_helper.py @@ -0,0 +1,230 @@ +import os +import re +import shutil +import uuid +from typing import Dict, Optional, Type + +import pytest_check as check +from pydantic import BaseModel +from src.objectModels.api_data_objects import ( + Entry, + FHIRImmunizationResponse, + ImmunizationReadResponse_IntTable, + PatientResource, + Search, +) +from src.objectModels.api_operation_outcome import OperationOutcome + +from utilities.date_helper import covert_to_expected_date_format +from utilities.error_constants import ERROR_MAP + + +def empty_folder(path): + if os.path.exists(path): + shutil.rmtree(path) + os.makedirs(path) + + +def find_entry_by_Imms_id(parsed_data, imms_id) -> Optional[object]: + return next( + ( + entry + for entry in parsed_data.entry + if entry.resource.resourceType == "Immunization" and entry.resource.id == imms_id + ), + None, + ) + + +def find_patient_by_fullurl(parsed_data) -> Optional[Entry]: + for entry in parsed_data.entry: + if entry.resource.resourceType == "Patient": + return entry + return None + + +RESOURCE_MAP: Dict[str, Type[BaseModel]] = { + "Immunization": FHIRImmunizationResponse, + "Patient": PatientResource, +} + + +def parse_entry(entry_data: dict) -> Entry: + resource_data = entry_data["resource"] + resource_type = resource_data.get("resourceType", "").lower() # ✅ Normalize case + + resource_class = RESOURCE_MAP.get(resource_type.capitalize()) # ✅ Match correct class + + if not resource_class: + raise ValueError(f"Unsupported resourceType: {resource_type}") + + parsed_resource = resource_class.parse_obj(resource_data) + parsed_search = Search.parse_obj(entry_data.get("search", {})) + + return Entry(fullUrl=entry_data.get("fullUrl"), resource=parsed_resource, search=parsed_search) + + +def is_valid_disease_type(disease_type: str) -> bool: + valid_types = { + "COVID", + "FLU", + "HPV", + "MMR", + "RSV", + "SHINGLES", + "MMRV", + "PNEUMOCOCCAL", + "MENACWY", + "PERTUSSIS", + "3IN1", + } + return disease_type in valid_types + + +def is_valid_nhs_number(nhs_number: str) -> bool: + nhs_number = nhs_number.replace(" ", "") + if not nhs_number.isdigit() or len(nhs_number) != 10: + return False + + digits = [int(d) for d in nhs_number] + total = sum((10 - i) * digits[i] for i in range(9)) + remainder = total % 11 + check_digit = 11 - remainder + if check_digit == 11: + check_digit = 0 + if check_digit == 10: + return False + return check_digit == digits[9] + + +def validate_error_response(error_response, errorName: str, imms_id: str = "", version: str = ""): + uuid_obj = uuid.UUID(error_response.id, version=4) + check.is_true(isinstance(uuid_obj, uuid.UUID), f"Id is not UUID {error_response.id}") + + fields_to_compare = [] + + match errorName: + case "not_found": + expected_diagnostics = ERROR_MAP.get("not_found", {}).get("diagnostics", "").replace("", imms_id) + fields_to_compare.append(("Diagnostics", expected_diagnostics, error_response.issue[0].diagnostics)) + + case "invalid_etag": + expected_diagnostics = ERROR_MAP.get("invalid_etag", {}).get("diagnostics", "").replace("", version) + fields_to_compare.append(("Diagnostics", expected_diagnostics, error_response.issue[0].diagnostics)) + case _: + actual_diagnostics = ( + error_response.issue[0] + .diagnostics.replace("- Date", "- Date") + .replace("offsets.\nNote", "offsets. Note") + .replace("\n_", " _") + .replace("_\n ", "_") + .replace("service.\n", "service.") + .replace("\n", "") + ) + expected_diagnostics = ERROR_MAP.get(errorName, {}).get("diagnostics", "") + fields_to_compare.append(("Diagnostics", expected_diagnostics, actual_diagnostics)) + + fields_to_compare.extend( + [ + ("ResourceType", ERROR_MAP.get("Common_field", {}).get("resourceType", ""), error_response.resourceType), + ("Meta_Profile", ERROR_MAP.get("Common_field", {}).get("profile", ""), error_response.meta.profile[0]), + ("Issue_Code", ERROR_MAP.get(errorName, {}).get("code", "").lower(), error_response.issue[0].code.lower()), + ( + "Coding_system", + ERROR_MAP.get("Common_field", {}).get("system", ""), + error_response.issue[0].details.coding[0].system, + ), + ( + "Coding_Code", + ERROR_MAP.get(errorName, {}).get("code", "").lower(), + error_response.issue[0].details.coding[0].code.lower(), + ), + ("severity", ERROR_MAP.get("Common_field", {}).get("severity", ""), error_response.issue[0].severity), + ] + ) + + for name, expected, actual in fields_to_compare: + check.is_true(expected == actual, f"Expected {name}: {expected}, got {actual}") + + +def parse_FHIR_immunization_response(json_data: dict) -> FHIRImmunizationResponse: + return FHIRImmunizationResponse.parse_obj(json_data) + + +def parse_read_response(json_data: dict) -> ImmunizationReadResponse_IntTable: + return ImmunizationReadResponse_IntTable.parse_obj(json_data) + + +def parse_error_response(json_data: dict) -> OperationOutcome: + return OperationOutcome.parse_obj(json_data) + + +def validate_to_compare_request_and_response(context, create_obj, created_event, table_validation: bool = False): + request_patient = create_obj.contained[1] + response_patient = created_event.patient + + expected_fullUrl = f"{context.baseUrl}/Immunization/{context.ImmsID}" + + referencePattern = r"^urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + expected_occurrenceDateTime = covert_to_expected_date_format(create_obj.occurrenceDateTime) + expected_recorded = covert_to_expected_date_format(create_obj.recorded) + actual_occurrenceDateTime = covert_to_expected_date_format(created_event.occurrenceDateTime) + actual_recorded = covert_to_expected_date_format(created_event.recorded) + + fields_to_compare = [] + + if not table_validation: + fields_to_compare.append(("fullUrl", expected_fullUrl, context.created_event.fullUrl)) + fields_to_compare.append( + ("patient.identifier.system", request_patient.identifier[0].system, response_patient.identifier.system) + ) + fields_to_compare.append( + ("patient.identifier.value", request_patient.identifier[0].value, response_patient.identifier.value) + ) + fields_to_compare.append( + ("patient.reference", bool(re.match(referencePattern, response_patient.reference)), True) + ) + fields_to_compare.append(("meta.versionId", context.expected_version, int(created_event.meta.versionId))) + + if table_validation: + fields_to_compare.append(("Contained", create_obj.contained, created_event.contained)) + fields_to_compare.append(("patient.reference", create_obj.patient.reference, created_event.patient.reference)) + fields_to_compare.append(("performer", create_obj.performer, created_event.performer)) + fields_to_compare.append(("Id", context.ImmsID, created_event.id)) + + fields_to_compare.extend( + [ + ("resourceType", create_obj.resourceType, created_event.resourceType), + ("extension", create_obj.extension, created_event.extension), + ("identifier.system", create_obj.identifier[0].system, created_event.identifier[0].system), + ("identifier.value", create_obj.identifier[0].value, created_event.identifier[0].value), + ("status", create_obj.status, created_event.status), + ("vaccineCode", create_obj.vaccineCode, created_event.vaccineCode), + ("patient.type", create_obj.patient.type, created_event.patient.type), + ("occurrenceDateTime", expected_occurrenceDateTime, actual_occurrenceDateTime), + ("Recorded", expected_recorded, actual_recorded), + ("primarySource", create_obj.primarySource, created_event.primarySource), + ("location", create_obj.location, created_event.location), + ("manufacturer", create_obj.manufacturer, created_event.manufacturer), + ("lotNumber", create_obj.lotNumber, created_event.lotNumber), + ("expirationDate", create_obj.expirationDate, created_event.expirationDate), + ("site", create_obj.site, created_event.site), + ("route", create_obj.route, created_event.route), + ("doseQuantity", create_obj.doseQuantity, created_event.doseQuantity), + # ("performer", create_obj.performer, created_event.performer), + ("reasonCode", create_obj.reasonCode, created_event.reasonCode), + ("protocolApplied", create_obj.protocolApplied, created_event.protocolApplied), + ] + ) + + for name, expected, actual in fields_to_compare: + check.is_true(expected == actual, f"Expected {name}: {expected}, Actual {actual}") + + +def extract_practitioner_name(response_practitioner): + name_entry = next(iter(response_practitioner.name or []), None) + + family = getattr(name_entry, "family", "") or "" + given = (getattr(name_entry, "given", []) or [""])[0] + + return {"Practitioner.name.family": family, "Practitioner.name.given": given} diff --git a/tests/e2e_automation/utilities/api_gen_token.py b/tests/e2e_automation/utilities/api_gen_token.py new file mode 100644 index 0000000000..3c879ead3c --- /dev/null +++ b/tests/e2e_automation/utilities/api_gen_token.py @@ -0,0 +1,86 @@ +import os +import uuid +from datetime import datetime, timedelta, timezone +from urllib.parse import parse_qs, urlparse + +import requests +from lxml import html + + +def extract_code(response): + qs = urlparse(response.history[-1].headers["Location"]).query + auth_code = parse_qs(qs)["code"] + if isinstance(auth_code, list): + auth_code = auth_code[0] + return auth_code + + +def get_access_token(context): + login_session = requests.session() + client_id = context.auth_client_Id + client_secret = context.auth_client_Secret + callback_url = context.callback_url + username = context.username + auth_url = context.auth_url + token_url = context.token_url + scope = context.scope + + # Login Page + authorize_resp = login_session.get( + auth_url, + params={ + "client_id": client_id, + "redirect_uri": callback_url, + "response_type": "code", + "scope": scope, + "state": str(uuid.uuid4()), + }, + ) + assert authorize_resp.status_code == 200, authorize_resp.text + + # Submit the login form + tree = html.fromstring(authorize_resp.content.decode()) + auth_form = tree.forms[0] + form_url = auth_form.action + form_data = {"username": username} + code_resp = login_session.post(url=form_url, data=form_data) + assert code_resp.status_code == 200, code_resp.text + + # Step3: extract code from redirect url + auth_code = extract_code(code_resp) + + # Step4: Post the code to get access token + token_resp = login_session.post( + token_url, + data={ + "grant_type": "authorization_code", + "code": auth_code, + "redirect_uri": callback_url, + "client_id": client_id, + "client_secret": client_secret, + }, + ) + assert token_resp.status_code == 200, token_resp.text + return token_resp.json()["access_token"] + + +def is_token_valid(token_expires_in_time, token_generated_time): + if token_expires_in_time is None or token_generated_time is None: + return False + expiration_time = token_generated_time + timedelta(seconds=int(token_expires_in_time)) + return datetime.now(timezone.utc) < expiration_time + + +def get_tokens(context, supplier_name): + env_vars_map = { + "auth_client_Secret": f"{supplier_name}_client_Secret", + "auth_client_Id": f"{supplier_name}_client_Id", + } + + for attr, env_var in env_vars_map.items(): + setattr(context, attr, os.getenv(env_var)) + + # if not is_token_valid(context.token_expires_in, context.token_gen_time): + # context.token, context.token_expires_in, context.token_gen_time = get_access_token(context) + + context.token = get_access_token(context) diff --git a/tests/e2e_automation/utilities/api_get_header.py b/tests/e2e_automation/utilities/api_get_header.py new file mode 100644 index 0000000000..9468b8a8f1 --- /dev/null +++ b/tests/e2e_automation/utilities/api_get_header.py @@ -0,0 +1,79 @@ +import uuid + + +def get_search_get_url_header(context): + context.url = context.baseUrl + "/Immunization" + context.headers = { + "X-Correlation-ID": str(uuid.uuid4()), + "X-Request-ID": str(uuid.uuid4()), + "Accept": "application/fhir+json", + "Authorization": "Bearer " + context.token, + } + context.corrID = context.headers["X-Correlation-ID"] + context.reqID = context.headers["X-Request-ID"] + + +def get_search_post_url_header(context): + context.url = context.baseUrl + "/Immunization/_search" + context.headers = { + "X-Correlation-ID": str(uuid.uuid4()), + "X-Request-ID": str(uuid.uuid4()), + "Accept": "application/fhir+json", + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Bearer " + context.token, + } + context.corrID = context.headers["X-Correlation-ID"] + context.reqID = context.headers["X-Request-ID"] + + +def get_create_post_url_header(context): + context.url = context.baseUrl + "/Immunization" + context.headers = { + "X-Correlation-ID": str(uuid.uuid4()), + "X-Request-ID": str(uuid.uuid4()), + "Accept": "application/fhir+json", + "Content-Type": "application/fhir+json", + "Authorization": "Bearer " + context.token, + } + context.corrID = context.headers["X-Correlation-ID"] + context.reqID = context.headers["X-Request-ID"] + + +def get_delete_url_header(context): + context.url = context.baseUrl + "/Immunization" + context.headers = { + "X-Correlation-ID": str(uuid.uuid4()), + "X-Request-ID": str(uuid.uuid4()), + "Accept": "application/fhir+json", + "Content-Type": "application/fhir+json", + "Authorization": "Bearer " + context.token if context.token is not None else "", + } + context.corrID = context.headers["X-Correlation-ID"] + context.reqID = context.headers["X-Request-ID"] + + +def get_update_url_header(context, tag: str): + context.url = context.baseUrl + "/Immunization" + context.headers = { + "X-Correlation-ID": str(uuid.uuid4()), + "X-Request-ID": str(uuid.uuid4()), + "Accept": "application/fhir+json", + "Content-Type": "application/fhir+json", + "E-Tag": tag, + "Authorization": "Bearer " + context.token, + } + context.corrID = context.headers["X-Correlation-ID"] + context.reqID = context.headers["X-Request-ID"] + + +def get_read_url_header(context): + context.url = context.baseUrl + f"/Immunization/{context.ImmsID}?_summary" + context.headers = { + "X-Correlation-ID": str(uuid.uuid4()), + "X-Request-ID": str(uuid.uuid4()), + "Accept": "application/fhir+json", + "Content-Type": "application/fhir+json", + "Authorization": "Bearer " + context.token, + } + context.corrID = context.headers["X-Correlation-ID"] + context.reqID = context.headers["X-Request-ID"] diff --git a/tests/e2e_automation/utilities/apigee/ApigeeApp.py b/tests/e2e_automation/utilities/apigee/ApigeeApp.py new file mode 100644 index 0000000000..3752b69e2a --- /dev/null +++ b/tests/e2e_automation/utilities/apigee/ApigeeApp.py @@ -0,0 +1,11 @@ +"""Simple data class to hold the required attributes of an Apigee App""" + +from dataclasses import dataclass + + +@dataclass +class ApigeeApp: + callback_url: str + client_id: str + client_secret: str + supplier: str diff --git a/tests/e2e_automation/utilities/apigee/ApigeeOnDemandAppManager.py b/tests/e2e_automation/utilities/apigee/ApigeeOnDemandAppManager.py new file mode 100644 index 0000000000..04194a9b1a --- /dev/null +++ b/tests/e2e_automation/utilities/apigee/ApigeeOnDemandAppManager.py @@ -0,0 +1,111 @@ +"""Basic client class for managing interactions with the Apigee API""" + +import uuid + +import requests + +from utilities.apigee.apigee_env_helpers import ( + get_apigee_access_token, + get_apigee_environment, + get_apigee_username, + get_proxy_name, +) +from utilities.apigee.ApigeeApp import ApigeeApp + + +class ApigeeOnDemandAppManager: + """Manager class that provides required Apigee functionality for Apigee non-prod env e2e tests, e.g. creating an + app, subscribing it to products and teardown""" + + # We only use the Apigee API in the non-prod organisation i.e. all environments except INT and PROD + _BASE_URL = "https://api.enterprise.apigee.com/v1/organizations/nhsd-nonprod" + _APPS_PATH = "apps" + _DEVELOPERS_PATH = "developers" + _PRODUCTS_PATH = "apiproducts" + _INTERNAL_DEV_ID_SERVICE_PRODUCT = "identity-service-internal-dev" + _TEST_APP_SUPPLIERS = ("EMIS", "MAVIS", "MEDICUS", "Postman_Auth", "RAVS", "SONAR", "TPP") + + def __init__(self): + self.proxy_name = get_proxy_name() + self.apigee_environment = get_apigee_environment() + self.created_product_name_uuid: str = "" + self.created_app_name_uuids = [] + self.display_name = f"test-{self.proxy_name}" + + self.logged_in_username = get_apigee_username() + self.access_token = get_apigee_access_token() + + self.requests_session = requests.Session() + self.requests_session.headers.update({"Authorization": f"Bearer {self.access_token}"}) + + def _create_app(self, target_product_name: str, supplier_name: str) -> ApigeeApp: + app_name_uuid = str(uuid.uuid4()) + app_data = { + "name": app_name_uuid, + "callbackUrl": "https://oauth.pstmn.io/v1/callback", + "status": "approved", + "attributes": [ + {"name": "DisplayName", "value": f"{self.display_name}-{supplier_name}"}, + {"name": "SupplierSystem", "value": supplier_name}, + ], + "apiProducts": [target_product_name, self._INTERNAL_DEV_ID_SERVICE_PRODUCT], + } + + response = self.requests_session.post( + url=f"{self._BASE_URL}/{self._DEVELOPERS_PATH}/{self.logged_in_username}/{self._APPS_PATH}", json=app_data + ) + response.raise_for_status() + + self.created_app_name_uuids.append(app_name_uuid) + response_dict = response.json() + + return ApigeeApp( + callback_url=response_dict.get("callbackUrl"), + client_id=response_dict["credentials"][0]["consumerKey"], + client_secret=response_dict["credentials"][0]["consumerSecret"], + supplier=supplier_name, + ) + + def _create_product(self) -> str: + product_name_uuid = str(uuid.uuid4()) + apigee_product_data = { + "name": product_name_uuid, + "apiResources": [], + "approvalType": "auto", + "description": "Autogenerated API product for E2E tests", + "displayName": self.display_name, + "environments": [self.apigee_environment], + "proxies": [self.proxy_name], + "scopes": [ + f"urn:nhsd:apim:app:level3:{self.proxy_name}", + f"urn:nhsd:apim:user-nhs-cis2:aal3:{self.proxy_name}", + ], + } + + response = self.requests_session.post( + url=f"{self._BASE_URL}/{self._PRODUCTS_PATH}", + json=apigee_product_data, + ) + response.raise_for_status() + + self.created_product_name_uuid = product_name_uuid + return product_name_uuid + + def setup_apps_and_product(self) -> list[ApigeeApp]: + """Orchestration method to set up the required product and on-demand apps required for PR testing""" + created_apps: list[ApigeeApp] = [] + product_name_uuid = self._create_product() + + for supplier_name in self._TEST_APP_SUPPLIERS: + created_apps.append(self._create_app(product_name_uuid, supplier_name)) + + return created_apps + + def teardown_apps_and_product(self): + """Orchestration method to remove the Apigee resources in a teardown step""" + for created_app_name_uuid in self.created_app_name_uuids: + self.requests_session.delete( + url=f"{self._BASE_URL}/{self._DEVELOPERS_PATH}/{self.logged_in_username}/{self._APPS_PATH}/{created_app_name_uuid}" + ) + + self.requests_session.delete(url=f"{self._BASE_URL}/{self._PRODUCTS_PATH}/{self.created_product_name_uuid}") diff --git a/tests/e2e_automation/utilities/apigee/__init__.py b/tests/e2e_automation/utilities/apigee/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_automation/utilities/apigee/apigee_env_helpers.py b/tests/e2e_automation/utilities/apigee/apigee_env_helpers.py new file mode 100644 index 0000000000..2470de6a94 --- /dev/null +++ b/tests/e2e_automation/utilities/apigee/apigee_env_helpers.py @@ -0,0 +1,51 @@ +import os + +PROXY_PR_PREFIX = "immunisation-fhir-api-pr-" +INT_PROXY_NAME = "immunisation-fhir-api-int" + + +def get_env_var(var_name: str) -> str: + value = os.getenv(var_name) + + if not value: + raise EnvironmentError(f"{var_name} environment variable is required") + + return value + + +def get_apigee_username() -> str: + return get_env_var("APIGEE_USERNAME") + + +def get_proxy_name() -> str: + return get_env_var("PROXY_NAME") + + +def use_temp_apigee_apps() -> bool: + """ + Determines if temporary Apigee Apps are required for the test run based on the following business logic: + - dynamic PR environments always require temporary apps + - Apigee non-prod environments (everything except INT and PROD) use dynamic apps unless the user provides the + USE_STATIC_APPS env var to override this + """ + if is_pr_env(): + return True + + if get_proxy_name() == INT_PROXY_NAME: + return False + + return os.getenv("USE_STATIC_APPS", "False") != "True" + + +def is_pr_env() -> bool: + """Checks if the tests are running against a dynamic PR environment""" + proxy_name = get_proxy_name() + return proxy_name.startswith(PROXY_PR_PREFIX) + + +def get_apigee_access_token() -> str: + return get_env_var("APIGEE_ACCESS_TOKEN") + + +def get_apigee_environment() -> str: + return get_env_var("APIGEE_ENVIRONMENT") diff --git a/tests/e2e_automation/utilities/aws_token.py b/tests/e2e_automation/utilities/aws_token.py new file mode 100644 index 0000000000..513308a00f --- /dev/null +++ b/tests/e2e_automation/utilities/aws_token.py @@ -0,0 +1,52 @@ +import logging +import os +import subprocess + +import boto3 + +logging.basicConfig(filename="debugLog.log", level=logging.INFO) +logger = logging.getLogger(__name__) + + +def set_aws_session_token(): + try: + print("token started.......") + # Create a session using your AWS credentials + session = boto3.Session() + + # Get the credentials object + credentials = session.get_credentials() + + # Refresh credentials if needed and retrieve the session token + credentials = credentials.get_frozen_credentials() + access_key = credentials.access_key + secret_key = credentials.secret_key + session_token = credentials.token + + # Set credentials as environment variables for the current process + os.environ["AWS_ACCESS_KEY_ID"] = access_key + os.environ["AWS_SECRET_ACCESS_KEY"] = secret_key + os.environ["AWS_SESSION_TOKEN"] = session_token + + # Use os.system to call AWS CLI commands and set credentials + os.system(f"aws configure set aws_access_key_id {access_key}") + os.system(f"aws configure set aws_secret_access_key {secret_key}") + os.system(f"aws configure set aws_session_token {session_token}") + + except Exception as e: + print(f"Error setting AWS session token: {e}") + + +def refresh_sso_token(profile_name): + try: + subprocess.run(["aws", "sso", "login", "--profile", profile_name], check=True) + print(f"SSO token refreshed for profile: {profile_name}") + + # After login, load credentials from the profile and set as env vars + session = boto3.Session(profile_name=profile_name) + credentials = session.get_credentials().get_frozen_credentials() + os.environ["AWS_ACCESS_KEY_ID"] = credentials.access_key + os.environ["AWS_SECRET_ACCESS_KEY"] = credentials.secret_key + os.environ["AWS_SESSION_TOKEN"] = credentials.token + except subprocess.CalledProcessError as e: + print(f"Error refreshing SSO token: {e}") diff --git a/tests/e2e_automation/utilities/batch_S3_buckets.py b/tests/e2e_automation/utilities/batch_S3_buckets.py new file mode 100644 index 0000000000..9c6adfa271 --- /dev/null +++ b/tests/e2e_automation/utilities/batch_S3_buckets.py @@ -0,0 +1,94 @@ +import time + +import boto3 +from botocore.exceptions import ClientError, NoCredentialsError + + +def upload_file_to_S3(context): + s3 = boto3.client("s3") + source_bucket = f"immunisation-batch-{context.S3_env}-data-sources" + file_path = f"{context.working_directory}/{context.filename}" + try: + s3.upload_file(file_path, source_bucket, context.filename) + print(f"Upload successful: {context.filename} → {source_bucket}") + except FileNotFoundError: + print(f"File not found: {file_path}") + except NoCredentialsError: + print("AWS credentials not available.") + except Exception as e: + print(f"Upload failed: {e}") + + +def wait_for_file_to_move_archive(context, timeout=120, interval=5): + s3 = boto3.client("s3") + source_bucket = f"immunisation-batch-{context.S3_env}-data-sources" + archive_key = f"archive/{context.filename}" + print(f"Waiting for file in archive: s3://{source_bucket}/{archive_key}") + elapsed = 0 + + while elapsed < timeout: + try: + s3.head_object(Bucket=source_bucket, Key=archive_key) + print(f"File found in archive: {archive_key}") + return True + except ClientError as e: + if e.response["Error"]["Code"] == "404": + print(f"Still waiting... ({elapsed}s)") + else: + print(f"Unexpected error: {e}") + return False + time.sleep(interval) + elapsed += interval + + print(f"Timeout: File not found in archive after {timeout} seconds") + return False + + +def wait_and_read_ack_file( + context, folderName: str, timeout=120, interval=5, duplicate_bus_files=False, duplicate_inf_files=False +): + s3 = boto3.client("s3") + destination_bucket = f"immunisation-batch-{context.S3_env}-data-destinations" + source_filename = context.filename + base_name = source_filename.replace(f".{context.file_extension}", "") + forwarded_prefix = f"{folderName}/{base_name}" + + context.forwarded_prefix = forwarded_prefix + + print(f"Waiting for file starting with '{forwarded_prefix}' in bucket: {destination_bucket}") + elapsed = 0 + + while elapsed < timeout: + try: + response = s3.list_objects_v2(Bucket=destination_bucket, Prefix=forwarded_prefix) + contents = response.get("Contents", []) + + if not contents: + print(f"[WAIT] No files found yet... ({elapsed}s)") + elif duplicate_inf_files and len(contents) == 1: + print(f"[WAIT] Waiting for more INF files... ({elapsed}s)") + elif duplicate_bus_files: + if len(contents) > 1: + print(f"[ERROR] Unexpected second BUS file detected: {contents}") + return "Unexpected duplicate BUS file found" + elif len(contents) == 1: + print(f"[WAIT] Only one BUS file seen so far... ({elapsed}s)") + else: + sorted_objects = sorted(contents, key=lambda obj: obj["LastModified"], reverse=True) + key = sorted_objects[0]["Key"] + print(f"[FOUND] File located: {key}") + + obj = s3.get_object(Bucket=destination_bucket, Key=key) + file_data = obj["Body"].read().decode("utf-8") + print(f"[SUCCESS] File contents loaded ({len(file_data)} bytes)") + return file_data + + time.sleep(interval) + elapsed += interval + + except ClientError as e: + print(f"[ERROR] S3 access failed: {e}") + return None + + print(f"[TIMEOUT] No file found with prefix '{forwarded_prefix}' after {timeout} seconds") + return None diff --git a/tests/e2e_automation/utilities/batch_api_mesh_mailbox_auth.py b/tests/e2e_automation/utilities/batch_api_mesh_mailbox_auth.py new file mode 100644 index 0000000000..33c1e09c5d --- /dev/null +++ b/tests/e2e_automation/utilities/batch_api_mesh_mailbox_auth.py @@ -0,0 +1,27 @@ +import datetime +import hmac +import os +import uuid +from hashlib import sha256 + + +def build_auth_header(mailbox_id: str, nonce: str = None, nonce_count: int = 0): + mailbox_id = os.getenv("MAILBOX_ID") + password = os.getenv("MAILBOX_PASSWORD") + auth_schema_name = os.getenv("AUTH_SCHEMA_NAME") + shared_key = os.getenv("SHARED_KEY") + + """ Generate MESH Authorization header for mailboxid. """ + # Generate a GUID if required. + if not nonce: + nonce = str(uuid.uuid4()) + # Current time formatted as yyyyMMddHHmm + # for example, 4th May 2020 13:05 would be 202005041305 + timestamp = datetime.utcnow().strftime("%Y%m%d%H%M") + + # for example, NHSMESH AMP01HC001:bd0e2bd5-218e-41d0-83a9-73fdec414803:0:202005041305 + hmac_msg = mailbox_id + ":" + nonce + ":" + str(nonce_count) + ":" + password + ":" + timestamp + + # HMAC is a standard crypto hash method built in the python standard library. + hash_code = hmac.HMAC(shared_key.encode(), hmac_msg.encode(), sha256).hexdigest() + return auth_schema_name + mailbox_id + ":" + nonce + ":" + str(nonce_count) + ":" + timestamp + ":" + hash_code diff --git a/tests/e2e_automation/utilities/batch_file_helper.py b/tests/e2e_automation/utilities/batch_file_helper.py new file mode 100644 index 0000000000..3cbc66e624 --- /dev/null +++ b/tests/e2e_automation/utilities/batch_file_helper.py @@ -0,0 +1,250 @@ +from utilities.error_constants import ERROR_MAP + + +def validate_bus_ack_file_for_successful_records(context, file_rows) -> bool: + if not file_rows: + print("No rows found in BUS ACK file for successful records") + return True + else: + success_mask = ~context.vaccine_df["UNIQUE_ID"].str.startswith("Fail-", na=False) & ( + context.vaccine_df["UNIQUE_ID"].str.strip() != "" + ) + + success_df = context.vaccine_df[success_mask] + + valid_ids = set(success_df["UNIQUE_ID"].astype(str) + "^" + success_df["UNIQUE_ID_URI"].astype(str)) + + file_ids = set(file_rows["LOCAL_ID"].astype(str)) + + intersection = valid_ids & file_ids + + if intersection: + print(f"Unexpected valid IDs found in BUS ACK file: {intersection}") + return False + else: + print("No successful records present in BUS ACK file — validation passed") + return True + + +def validate_inf_ack_file(context, success: bool = True) -> bool: + content = context.fileContent + lines = content.strip().split("\n") + header = lines[0].split("|") + row = lines[1].split("|") + expected_columns = 12 + + if len(header) != expected_columns: + print(f"Header column count mismatch: expected {expected_columns}, got {len(header)}") + return False + + row_valid = True # Reset for each row + + if len(row) != expected_columns: + print(f"Row column count mismatch: expected {expected_columns} got {len(row)}") + return False + + header_response_code = row[1] + issue_severity = row[2] + issue_code = row[3] + response_code = row[6] + response_display = row[7] + message_delivery = row[11] + + if success: + expected_message_delivery = "True" + excepted_header_response_code = "Success" + excepted_issue_severity = "Information" + excepted_issue_code = "OK" + excepted_response_code = "20013" + expected_response_display = "Success" + else: + expected_message_delivery = "False" + excepted_header_response_code = "Failure" + excepted_issue_severity = "Fatal" + excepted_issue_code = "Fatal Error" + excepted_response_code = "10002" + expected_response_display = "Infrastructure Level Response Value - Processing Error" + + if header_response_code != excepted_header_response_code: + print(f"HEADER_RESPONSE_CODE is not {excepted_header_response_code}") + row_valid = False + if issue_severity != excepted_issue_severity: + print(f"ISSUE_SEVERITY is not {excepted_issue_severity}") + row_valid = False + if issue_code != excepted_issue_code: + print(f"ISSUE_CODE is not {excepted_issue_code}") + row_valid = False + if response_code != excepted_response_code: + print(f"RESPONSE_CODE is not {excepted_response_code}") + row_valid = False + if response_display != expected_response_display: + print(f"RESPONSE_DISPLAY is not {expected_response_display}") + row_valid = False + if message_delivery != expected_message_delivery: + print(f"MESSAGE_DELIVERY is not {expected_message_delivery}") + row_valid = False + + return row_valid + + +def normalize_for_lookup(id_str: str) -> str: + parts = str(id_str).split("^") + prefix = parts[0].strip() if len(parts) > 0 else "" + suffix = parts[1].strip() if len(parts) > 1 else "" + normalized_prefix = "" if prefix in ["", "nan"] else prefix + normalized_suffix = "" if suffix in ["", "nan"] else suffix + return f"{normalized_prefix}^{normalized_suffix}" + + +def validate_bus_ack_file_for_error(context, file_rows) -> bool: + if not file_rows: + print("No rows found in BUS ACK file for failed records") + return False + + fail_mask = context.vaccine_df["UNIQUE_ID"].str.startswith("Fail-", na=False) | ( + context.vaccine_df["UNIQUE_ID"].str.strip() == "" + ) + + fail_df = context.vaccine_df[fail_mask] + + valid_ids = set(fail_df["UNIQUE_ID"].astype(str) + "^" + fail_df["UNIQUE_ID_URI"].astype(str)) + + overall_valid = True + + for valid_id in valid_ids: + normalized_id = normalize_for_lookup(valid_id) + row_data_list = file_rows.get(normalized_id) + + if not row_data_list: + print(f"Valid ID '{valid_id}' not found in file") + overall_valid = False + continue + + for row_data in row_data_list: + i = row_data["row"] + fields = row_data["fields"] + row_valid = True + + header_response_code = fields[1] + issue_severity = fields[2] + issue_code = fields[3] + response_code = fields[6] + response_display = fields[7] + imms_id = fields[11] + operation_outcome = fields[12] + message_delivery = fields[13] + + if header_response_code != "Fatal Error": + print(f"Row {i}: HEADER_RESPONSE_CODE is not 'Fatal Error'") + row_valid = False + if issue_severity != "Fatal": + print(f"Row {i}: ISSUE_SEVERITY is not 'Fatal'") + row_valid = False + if issue_code != "Fatal Error": + print(f"Row {i}: ISSUE_CODE is not 'Fatal Error'") + row_valid = False + if response_code != "30002": + print(f"Row {i}: RESPONSE_CODE is not '30002'") + row_valid = False + if response_display != "Business Level Response Value - Processing Error": + print(f"Row {i}: RESPONSE_DISPLAY is not expected value") + row_valid = False + if imms_id: + print(f"Row {i}: IMMS_ID is populated but should be null") + row_valid = False + if message_delivery != "False": + print(f"Row {i}: MESSAGE_DELIVERY is not 'False'") + row_valid = False + + try: + valid_id_df = context.vaccine_df.loc[i - 2] + prefix = str(valid_id_df["UNIQUE_ID"]).strip() + + if prefix in ["", " ", "nan"]: + expected_error = valid_id_df["PERSON_SURNAME"] if not valid_id_df.empty else "no_valid_surname" + + else: + split_parts = prefix.split("-") + expected_error = split_parts[2] if len(split_parts) > 2 else "invalid_prefix_format" + + expected_diagnostic = ERROR_MAP.get(expected_error, {}).get("diagnostics") + + if operation_outcome != expected_diagnostic: + print( + f"Row {i}: operation_outcome does not match expected diagnostics '{expected_diagnostic}' for '{expected_error}' but got '{operation_outcome}'" + ) + row_valid = False + + except Exception as e: + print(f"Row {i}: error extracting expected diagnostics from local_id '{valid_id}': {e}") + row_valid = False + + overall_valid = overall_valid and row_valid + + return overall_valid + + +def read_and_validate_bus_ack_file_content(context, by_local_id: bool = True, by_row_number: bool = False) -> dict: + # Prevent invalid combinations + if by_local_id and by_row_number: + raise ValueError("Choose only one mode: by_local_id OR by_row_number") + + content = context.fileContent.strip() + lines = content.split("\n") + + expected_header = [ + "MESSAGE_HEADER_ID", + "HEADER_RESPONSE_CODE", + "ISSUE_SEVERITY", + "ISSUE_CODE", + "ISSUE_DETAILS_CODE", + "RESPONSE_TYPE", + "RESPONSE_CODE", + "RESPONSE_DISPLAY", + "RECEIVED_TIME", + "MAILBOX_FROM", + "LOCAL_ID", + "IMMS_ID", + "OPERATION_OUTCOME", + "MESSAGE_DELIVERY", + ] + + if not lines: + print("File is empty") + return {} + + header = lines[0].split("|") + if header != expected_header: + print("Header mismatch") + return {} + + file_rows = {} + + if by_local_id: + for i, line in enumerate(lines[1:], start=2): + fields = line.split("|") + local_id = normalize_for_lookup(fields[10]) + + file_rows.setdefault(local_id, []).append( + { + "row": i, + "fields": fields, + "original_local_id": fields[10], + } + ) + return file_rows + + if by_row_number: + for i, line in enumerate(lines[1:], start=2): + fields = line.split("|") + + file_rows[i] = [ + { + "row": i, + "fields": fields, + "original_local_id": fields[10], + } + ] + return file_rows + + raise ValueError("You must select either by_local_id=True or by_row_number=True") diff --git a/tests/e2e/utils/compute_totp_code.py b/tests/e2e_automation/utilities/compute_totp_code.py similarity index 100% rename from tests/e2e/utils/compute_totp_code.py rename to tests/e2e_automation/utilities/compute_totp_code.py diff --git a/tests/e2e_automation/utilities/context.py b/tests/e2e_automation/utilities/context.py new file mode 100644 index 0000000000..e74abedd6d --- /dev/null +++ b/tests/e2e_automation/utilities/context.py @@ -0,0 +1,45 @@ +class ScenarioContext: + def __init__(self): + self.supplier_name = None + self.token = None + self.token_expires_in = None + self.token_gen_time = None + self.patient_id = None + self.vaccine_type = None + self.patient = None + self.immunization_object = None + self.url = None + self.headers = None + self.corrID = None + self.reqID = None + self.json_request = None + self.response = None + self.response_json = None + self.auth_url = None + self.token_url = None + self.callback_url = None + self.auth_client_Id = None + self.auth_client_Secret = None + self.username = None + self.scope = None + self.ImmsID = None + self.request = None + self.baseUrl = None + self.location = None + self.parsed_search__object = None + self.created_event = None + self.create_object = None + self.Patient_fullUrl = None + self.params = None + self.soft_assertions = None + self.aws_profile_name = None + self.expected_version = 1 + self.eTag = None + self.S3_env = None + self.fileName = None + self.vaccine_df = None + self.file_extension = "csv" + self.supplier_ods_code = None + self.working_directory = None + self.fileContent = None + self.delta_cache = None diff --git a/tests/e2e_automation/utilities/date_helper.py b/tests/e2e_automation/utilities/date_helper.py new file mode 100644 index 0000000000..ebb1b72bb6 --- /dev/null +++ b/tests/e2e_automation/utilities/date_helper.py @@ -0,0 +1,71 @@ +from datetime import datetime, timedelta, timezone + + +def format_timestamp(timestamp): + parts = timestamp.split(".") + + if len(parts) == 2: + milliseconds, timezone = parts[1].split("+") + milliseconds = milliseconds.ljust(6, "0") + + return f"{parts[0]}.{milliseconds}+{timezone}" + + +def covert_to_expected_date_format(date_string: str) -> str: + try: + dt = datetime.fromisoformat(date_string.replace("Z", "+00:00")) + return dt.isoformat() + except ValueError: + return "Invalid format" + + +def iso_to_compact(dt_str): + dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00")) + return dt.strftime("%Y%m%dT%H%M%S00") + + +def is_valid_date(date_str: str) -> bool: + try: + datetime.strptime(date_str, "%Y-%m-%d") + return True + except ValueError: + return False + + +def generate_date(date_str: str) -> str: + now = datetime.now(timezone.utc) + match date_str: + case "future_occurrence": + return (now + timedelta(seconds=500)).isoformat(timespec="milliseconds") + case "past_occurrence": + return (now - timedelta(seconds=5050)).isoformat(timespec="milliseconds") + case "current_occurrence_with_milliseconds": + now = now - timedelta(seconds=50) + return now.strftime("%Y%m%dT%H%M%S") + "00" + case "invalid_batch_occurrence": + return now.isoformat(timespec="milliseconds") + case "current_occurrence": + return (now - timedelta(seconds=60)).isoformat(timespec="milliseconds") + case "current_date": + return str(now.date()) + case "future_date": + return str((now + timedelta(days=1)).date()) + case "past_date": + return str((now - timedelta(days=1)).date()) + case "invalid_format": + return "2023/23/01" + case "nonexistent": + return "2023-02-30T10:00:00.000Z" + case "empty": + return "" + case "none": + return None + case _: + raise ValueError(f"Unknown date type: {date_str}") + + +def format_date_yyyymmdd(date_str: str) -> str: + try: + return datetime.strptime(date_str, "%Y%m%d").strftime("%Y-%m-%d") + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid date format: {date_str}") from e diff --git a/tests/e2e_automation/utilities/enums.py b/tests/e2e_automation/utilities/enums.py new file mode 100644 index 0000000000..0e0af56db1 --- /dev/null +++ b/tests/e2e_automation/utilities/enums.py @@ -0,0 +1,49 @@ +from enum import Enum + + +class Operation(Enum): + created = "CREATE" + updated = "UPDATE" + deleted = "DELETE" + + +class ActionFlag(Enum): + created = "NEW" + updated = "UPDATE" + deleted = "DELETE" + + +class SupplierNameWithODSCode(Enum): + MAVIS = "V0V8L" + SONAR = "8HK48" + RAVS = "X8E5B" + PINNACLE = "8J1100001" + EMIS = "YGJ" + TPP = "YGA" + MEDICUS = "YGMYW" + CEGEDIM = "YGM04" + Postman_Auth = "Postman_Auth" + + +class GenderCode(Enum): + male = "1" + female = "2" + unknown = "0" + other = "9" + + +class ActionMap(Enum): + new = (Operation.created, ActionFlag.created) + update = (Operation.updated, ActionFlag.updated) + delete = (Operation.deleted, ActionFlag.deleted) + created = (Operation.created, ActionFlag.created) + updated = (Operation.updated, ActionFlag.updated) + deleted = (Operation.deleted, ActionFlag.deleted) + + @property + def operation(self): + return self.value[0] + + @property + def action_flag(self): + return self.value[1] diff --git a/tests/e2e_automation/utilities/error_constants.py b/tests/e2e_automation/utilities/error_constants.py new file mode 100644 index 0000000000..fb6dfc627b --- /dev/null +++ b/tests/e2e_automation/utilities/error_constants.py @@ -0,0 +1,245 @@ +ERROR_MAP = { + "Common_field": { + "resourceType": "OperationOutcome", + "profile": "https://simplifier.net/guide/UKCoreDevelopment2/ProfileUKCore-OperationOutcome", + "system": "https://fhir.nhs.uk/Codesystem/http-error-codes", + "severity": "error", + }, + "invalid_DateFrom_Include": { + "code": "INVALID", + "diagnostics": "Search parameter -date.from must be in format: YYYY-MM-DD; Search parameter _include may only be 'Immunization:patient' if provided.", + }, + "invalid_NHSNumber": { + "code": "INVALID", + "diagnostics": "Search parameter patient.identifier must be a valid NHS number.", + }, + "empty_NHSNumber": { + "code": "INVALID", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value must be a non-empty string", + }, + "invalid_include": { + "code": "INVALID", + "diagnostics": "Search parameter _include may only be 'Immunization:patient' if provided.", + }, + "invalid_DateFrom_To": { + "code": "INVALID", + "diagnostics": "Search parameter -date.from must be in format: YYYY-MM-DD; Search parameter -date.to must be in format: YYYY-MM-DD", + }, + "invalid_DateFrom_DateTo_Include": { + "code": "INVALID", + "diagnostics": "Search parameter -date.from must be in format: YYYY-MM-DD; Search parameter -date.to must be in format: YYYY-MM-DD; Search parameter _include may only be 'Immunization:patient' if provided.", + }, + "invalid_DiseaseType": { + "code": "INVALID", + "diagnostics": "-immunization.target must be one or more of the following: ROTAVIRUS, RSV, SHINGLES, 6IN1, MMR, FLU, 3IN1, PERTUSSIS, MENB, HIB, MMRV, BCG, MENACWY, 4IN1, COVID, PNEUMOCOCCAL, HPV, HEPB", + }, + "invalid_DateFrom": {"code": "INVALID", "diagnostics": "Search parameter -date.from must be in format: YYYY-MM-DD"}, + "invalid_DateTo": {"code": "INVALID", "diagnostics": "Search parameter -date.to must be in format: YYYY-MM-DD"}, + "unauthorized_access": {"code": "FORBIDDEN", "diagnostics": "Unauthorized request for vaccine type"}, + "not_found": {"code": "NOT-FOUND", "diagnostics": "Immunization resource does not exist. ID: "}, + "forbidden": {"code": "FORBIDDEN", "diagnostics": "Unauthorized request for vaccine type"}, + "doseNumberPositiveInt_PositiveInteger": { + "code": "INVARIANT", + "diagnostics": "Validation errors: protocolApplied[0].doseNumberPositiveInt must be a positive integer", + }, + "doseNumberPositiveInt_ValidRange": { + "code": "INVARIANT", + "diagnostics": "Validation errors: protocolApplied[0].doseNumberPositiveInt must be an integer in the range 1 to 9", + }, + "invalid_OccurrenceDateTime": { + "code": "INVARIANT", + "diagnostics": "Validation errors: occurrenceDateTime must be a valid datetime in one of the following formats:- 'YYYY-MM-DD' — Full date only- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone- Date must not be in the future.Only '+00:00' and '+01:00' are accepted as valid timezone offsets. Note that partial dates are not allowed for occurrenceDateTime in this service.", + }, + "empty_OccurrenceDateTime": { + "code": "INVARIANT", + "diagnostics": "1 validation error for Immunization __root__ Expect any of field value from this list ['occurrenceDateTime', 'occurrenceString']. (type=value_error)", + }, + "invalid_recorded": { + "code": "INVARIANT", + "diagnostics": "Validation errors: recorded must be a valid datetime in one of the following formats:- 'YYYY-MM-DD' — Full date only- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone- Date must not be in the future.", + }, + "empty_recorded": {"code": "INVARIANT", "diagnostics": "Validation errors: recorded is a mandatory field"}, + "future_DateOfBirth": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].birthDate must not be in the future", + }, + "missing_DateOfBirth": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].birthDate is a mandatory field", + }, + "invalid_DateOfBirth": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].birthDate must be a valid date string in the format \"YYYY-MM-DD\"", + }, + "invalid_expirationDate": { + "code": "INVARIANT", + "diagnostics": 'Validation errors: expirationDate must be a valid date string in the format "YYYY-MM-DD"', + }, + "invalid_nhsnumber_length": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value must be 10 characters", + }, + "no_forename": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].given is a mandatory field", + }, + "empty_forename": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].given must be an array", + }, + "empty_array_item_forename": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].given[0] must be a non-empty string", + }, + "no_surname": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].family is a mandatory field", + }, + "empty_forename_surname": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].given is a mandatory field; contained[?(@.resourceType=='Patient')].name[0].family is a mandatory field", + }, + "empty_surname": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].family must be a non-empty string", + }, + "invalid_gender": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].gender must be one of the following: male, female, other, unknown", + }, + "empty_gender": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].gender must be a non-empty string", + }, + "missing_gender": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].gender is a mandatory field", + }, + "invalid_mod11_nhsnumber": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].identifier[0].value is not a valid NHS number", + }, + "should_be_string": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].gender must be a string", + }, + "max_len_surname": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].family must be 35 or fewer characters", + }, + "max_len_forename": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].given[0] must be 180 or fewer characters", + }, + "max_item_forename": { + "code": "INVARIANT", + "diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].name[0].given must be an array of maximum length 5", + }, + "empty_site_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: performer[?(@.actor.type=='Organization')].actor.identifier.value is a mandatory field", + }, + "no_site_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: performer[?(@.actor.type=='Organization')].actor.identifier.value must be a non-empty string", + }, + "empty_site_code_uri": { + "code": "INVARIANT", + "diagnostics": "Validation errors: performer[?(@.actor.type=='Organization')].actor.identifier.system is a mandatory field", + }, + "no_site_code_uri": { + "code": "INVARIANT", + "diagnostics": "Validation errors: performer[?(@.actor.type=='Organization')].actor.identifier.system must be a non-empty string", + }, + "empty_location_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: location.identifier.value is a mandatory field", + }, + "no_location_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: location.identifier.value must be a non-empty string", + }, + "empty_location_code_uri": { + "code": "INVARIANT", + "diagnostics": "Validation errors: location.identifier.system is a mandatory field", + }, + "no_location_code_uri": { + "code": "INVARIANT", + "diagnostics": "Validation errors: location.identifier.system must be a non-empty string", + }, + "no_unique_identifiers": {"code": "INVARIANT", "diagnostics": "UNIQUE_ID or UNIQUE_ID_URI is missing"}, + "no_unique_id": { + "code": "INVARIANT", + "diagnostics": "Validation errors: identifier[0].value must be a non-empty string", + }, + "no_unique_id_uri": { + "code": "INVARIANT", + "diagnostics": "Validation errors: identifier[0].system must be a non-empty string", + }, + "empty_primary_source": { + "code": "INVARIANT", + "diagnostics": "Validation errors: primarySource is a mandatory field", + }, + "no_primary_source": {"code": "INVARIANT", "diagnostics": "Validation errors: primarySource must be a boolean"}, + "no_procedure_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding[?(@.system=='http://snomed.info/sct')].code is a mandatory field", + }, + "empty_procedure_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding[?(@.system=='http://snomed.info/sct')].code must be a non-empty string", + }, + "empty_product_term": { + "code": "INVARIANT", + "diagnostics": "Validation errors: vaccineCode.coding[?(@.system=='http://snomed.info/sct')].display must be a non-empty string", + }, + "empty_product_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: vaccineCode.coding[?(@.system=='http://snomed.info/sct')].code must be a non-empty string", + }, + "empty_procedure_term": { + "code": "INVARIANT", + "diagnostics": "Validation errors: extension[?(@.url=='https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure')].valueCodeableConcept.coding[?(@.system=='http://snomed.info/sct')].display must be a non-empty string", + }, + "invalid_action_flag": { + "code": "INVARIANT", + "diagnostics": "Invalid ACTION_FLAG - ACTION_FLAG must be 'NEW', 'UPDATE' or 'DELETE'", + }, + "empty_manufacturer": { + "code": "INVARIANT", + "diagnostics": "Validation errors: manufacturer.display must be a non-empty string", + }, + "empty_lot_number": {"code": "INVARIANT", "diagnostics": "Validation errors: lotNumber must be a non-empty string"}, + "empty_vaccine_site_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: site.coding[?(@.system=='http://snomed.info/sct')].code must be a non-empty string", + }, + "empty_vaccine_site_term": { + "code": "INVARIANT", + "diagnostics": "Validation errors: site.coding[?(@.system=='http://snomed.info/sct')].display must be a non-empty string", + }, + "empty_route_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: route.coding[?(@.system=='http://snomed.info/sct')].code must be a non-empty string", + }, + "empty_route_term": { + "code": "INVARIANT", + "diagnostics": "Validation errors: route.coding[?(@.system=='http://snomed.info/sct')].display must be a non-empty string", + }, + "empty_doseQuantity_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: doseQuantity.code must be a non-empty string", + }, + "empty_doseQuantity_term": { + "code": "INVARIANT", + "diagnostics": "Validation errors: doseQuantity.unit must be a non-empty string", + }, + "empty_indication_code": { + "code": "INVARIANT", + "diagnostics": "Validation errors: reasonCode[0].coding[0].code must be a non-empty string", + }, + "invalid_etag": { + "code": "INVARIANT", + "diagnostics": "Validation errors: Immunization resource version: in the request headers is invalid.", + }, +} diff --git a/tests/e2e_automation/utilities/http_requests_session.py b/tests/e2e_automation/utilities/http_requests_session.py new file mode 100644 index 0000000000..6aba940b1d --- /dev/null +++ b/tests/e2e_automation/utilities/http_requests_session.py @@ -0,0 +1,11 @@ +"""A simple global HTTP requests session. It will retry three times with backoff for any 502 errors of any HTTP method. +This is due to a known issue with the Apigee -> AWS APIGW backend where intermittent 502 errors can be seen when +initially ramping up traffic""" + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +retry_strategy = Retry(total=3, allowed_methods=None, status_forcelist=[502], backoff_factor=1) +http_requests_session = requests.Session() +http_requests_session.mount("https://", HTTPAdapter(max_retries=retry_strategy)) diff --git a/tests/e2e_automation/utilities/text_helper.py b/tests/e2e_automation/utilities/text_helper.py new file mode 100644 index 0000000000..f698f784e9 --- /dev/null +++ b/tests/e2e_automation/utilities/text_helper.py @@ -0,0 +1,34 @@ +import random +import string +from typing import Optional, Union + + +def get_text(text_str: str) -> Optional[Union[str, int]]: + match text_str: + case "missing": + return None + case "empty": + return "" + case "number": + return random.randint(0, 9) + case "gender_code": + return "1" + case "random_text": + return "random" + case "white_space": + return " " + case "white_space_array": + return '[ " " ]' + case _ if text_str.startswith("name_length_"): + try: + length = int(text_str.split("_")[2]) + return generate_random_length_name(length) + except (IndexError, ValueError): + raise ValueError(f"Invalid format for name_length: '{text_str}'") + case _: + raise ValueError(f"Unknown text type: '{text_str}'") + + +def generate_random_length_name(length=20) -> str: + name = "".join(random.choices(string.ascii_letters, k=length)) + return name.capitalize() diff --git a/tests/e2e_automation/utilities/vaccination_constants.py b/tests/e2e_automation/utilities/vaccination_constants.py new file mode 100644 index 0000000000..2de8376a6f --- /dev/null +++ b/tests/e2e_automation/utilities/vaccination_constants.py @@ -0,0 +1,753 @@ +VACCINE_CODE_MAP = { + "COVID": [ + { + "system": "http://snomed.info/sct", + "code": "43111411000001101", + "display": "Comirnaty JN.1 COVID-19 mRNA Vaccine 30micrograms/0.3ml dose dispersion for injection multidose vials (Pfizer Ltd)", + "stringValue": "Comirnaty JN.1 COVID-19 mRNA Vaccine 30micrograms/0.3ml dose dispersion for injection multidose vials", + "idValue": "4311141100001101", + "manufacturer": "Pfizer Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "43113211000001101", + "display": "Comirnaty JN.1 Children 6 months - 4 years COVID-19 mRNA Vaccine 3micrograms/0.3ml dose concentrate for dispersion for injection multidose vials (Pfizer Ltd)", + "stringValue": "Comirnaty JN.1 Children 6 months - 4 years COVID-19 mRNA Vaccine 3micrograms/0.3ml dose concentrate for dispersion for injection multidose vials", + "idValue": "4311321100001101", + "manufacturer": "Pfizer Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "43112711000001100", + "display": "Comirnaty JN.1 Children 5-11 years COVID-19 mRNA Vaccine 10micrograms/0.3ml dose dispersion for injection single dose vials (Pfizer Ltd)", + "stringValue": "Comirnaty JN.1 Children 5-11 years COVID-19 mRNA Vaccine 10micrograms/0.3ml dose dispersion for injection single dose vials", + "idValue": "431127110001100", + "manufacturer": "Pfizer Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "42985911000001104", + "display": "Spikevax JN.1 COVID-19 mRNA Vaccine 0.1mg/ml dispersion for injection multidose vials (Moderna, Inc)", + "stringValue": "Spikevax JN.1 COVID-19 mRNA Vaccine 0.1mg/ml dispersion for injection multidose vials", + "idValue": "4298591100001104", + "manufacturer": "Moderna, Inc", + }, + ], + "FLU": [ + { + "system": "http://snomed.info/sct", + "code": "34680411000001107", + "display": "Quadrivalent influenza vaccine (split virion, inactivated) suspension for injection 0.5ml pre-filled syringes (Sanofi Pasteur)", + "stringValue": "Quadrivalent influenza vaccine (split virion, inactivated) suspension for injection 0.5ml pre-filled syringes", + "idValue": "3468041100001107", + "manufacturer": "Sanofi Pasteur", + } + ], + "RSV": [ + { + "system": "http://snomed.info/sct", + "code": "42223111000001107", + "display": "Arexvy vaccine powder and suspension for suspension for injection 0.5ml vials (GlaxoSmithKline UK Ltd)", + "stringValue": "Arexvy vaccine powder and suspension for suspension for injection 0.5ml vials", + "idValue": "4222311100001107", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "42605811000001109", + "display": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials (Pfizer Ltd)", + "stringValue": "Abrysvo vaccine powder and solvent for solution for injection 0.5ml vials", + "idValue": "4260581100001109", + "manufacturer": "Pfizer Ltd", + }, + ], + "HPV": [ + { + "system": "http://snomed.info/sct", + "code": "12238911000001100", + "display": "Cervarix vaccine suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline) (product)", + "stringValue": "Gardasil 9 suspension for injection 0.5ml pre-filled syringes", + "idValue": "12238911000001100", + "manufacturer": "GlaxoSmithKline", + }, + { + "system": "http://snomed.info/sct", + "code": "33493111000001108", + "display": "Gardasil 9 vaccine suspension for injection 0.5ml pre-filled syringes (Merck Sharp & Dohme (UK) Ltd) (product)", + "stringValue": "Gardasil 9 suspension for injection 0.5ml pre-filled syringes", + "idValue": "33493111000001108", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "10880211000001104", + "display": "Gardasil vaccine suspension for injection 0.5ml pre-filled syringes (Merck Sharp & Dohme (UK) Ltd) (product)", + "stringValue": "Gardasil 9 suspension for injection 0.5ml pre-filled syringes", + "idValue": "10880211000001104", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd", + }, + ], + "MENACWY": [ + { + "system": "http://snomed.info/sct", + "code": "39779611000001104", + "display": "MenQuadfi vaccine solution for injection 0.5ml vials (Sanofi) (product)", + "stringValue": "MenQuadfi vaccine solution for injection 0.5ml vials", + "idValue": "3977961100001104", + "manufacturer": "Sanofi", + }, + { + "system": "http://snomed.info/sct", + "code": "20517811000001104", + "display": "Nimenrix vaccine powder and solvent for solution for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd) (product)", + "stringValue": "Nimenrix vaccine powder and solvent for solution for injection 0.5ml pre-filled syringes", + "idValue": "20517811000001104", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "17188711000001105", + "display": "Menveo vaccine powder and solvent for solution for injection 0.5ml vials (Novartis Vaccines and Diagnostics Ltd) (product)", + "stringValue": "Menveo vaccine powder and solvent for solution for injection 0.5ml vials", + "idValue": "17188711000001105", + "manufacturer": "Novartis Vaccines and Diagnostics Ltd", + }, + ], + "3IN1": [ + { + "system": "http://snomed.info/sct", + "code": "7374511000001107", + "display": "Revaxis vaccine suspension for injection 0.5ml pre-filled syringes (Sanofi) 1 pre-filled disposable injection (product)", + "stringValue": "Revaxis vaccine suspension for injection 0.5ml pre-filled syringes ", + "idValue": "7374511000001107", + "manufacturer": "Sanofi", + }, + ], + "MMR": [ + { + "system": "http://snomed.info/sct", + "code": "13968211000001108", + "display": "M-M-RVAXPRO vaccine powder and solvent for suspension for injection 0.5ml pre-filled syringes (Merck Sharp & Dohme (UK) Ltd)", + "stringValue": "M-M-RVAXPRO vaccine powder and solvent for suspension for injection 0.5ml pre-filled syringes", + "idValue": "13968211000001108", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "34925111000001104", + "display": "Priorix vaccine powder and solvent for solution for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "stringValue": "Priorix vaccine powder and solvent for solution for injection 0.5ml pre-filled syringes", + "idValue": "34925111000001104", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + ], + "MMRV": [ + { + "system": "http://snomed.info/sct", + "code": "45525711000001102", + "display": "Priorix Tetra vaccine powder and solvent for solution for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "stringValue": "Priorix Tetra vaccine powder and solvent for solution for injection 0.5ml pre-filled syringes", + "idValue": "45525711000001102", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "45480711000001107", + "display": "ProQuad vaccine powder and solvent for suspension for injection 0.5ml pre-filled syringes (Merck Sharp & Dohme (UK) Ltd)", + "stringValue": "ProQuad vaccine powder and solvent for suspension for injection 0.5ml pre-filled syringes", + "idValue": "45480711000001107", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd", + }, + ], + "PERTUSSIS": [ + { + "system": "http://snomed.info/sct", + "code": "42707511000001109", + "display": "Adacel vaccine suspension for injection 0.5ml pre-filled syringes (Sanofi)", + "stringValue": "Adacel vaccine suspension for injection 0.5ml pre-filled syringes", + "idValue": "42707511000001109", + "manufacturer": "Sanofi", + }, + { + "system": "http://snomed.info/sct", + "code": "26267211000001100", + "display": "Boostrix-IPV suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd) (product)", + "stringValue": "Boostrix-IPV suspension for injection 0.5ml pre-filled syringes ", + "idValue": "26267211000001100", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + ], + "SHINGLES": [ + { + "system": "http://snomed.info/sct", + "code": "39655511000001105", + "display": "Shingrix (Herpes Zoster) adjuvanted recombinant vaccine powder and suspension for suspension for injection 0.5ml vials (GlaxoSmithKline UK Ltd)", + "stringValue": "Shingrix (Herpes Zoster) adjuvanted recombinant vaccine powder and suspension for suspension for injection 0.5ml vials", + "idValue": "39655511000001105", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "38737511000001105", + "display": "Shingles (Herpes Zoster) adjuvanted recombinant vaccine powder and solvent for suspension for injection 0.5ml vials", + "stringValue": "Shingles adjuvanted recombinant vaccine powder and solvent for suspension for injection 0.5ml vials", + "idValue": "38737511000001105", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + ], + "PNEUMOCOCCAL": [ + { + "system": "http://snomed.info/sct", + "code": "16649411000001104", + "display": "Prevenar 13 vaccine suspension for injection 0.5ml pre-filled syringes (Pfizer Ltd)", + "stringValue": "Prevenar 13 vaccine suspension for injection 0.5ml pre-filled syringes", + "idValue": "16649411000001104", + "manufacturer": "Pfizer Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "40600011000001101", + "display": "Prevenar 20 vaccine suspension for injection 0.5ml pre-filled syringes (Pfizer Ltd)", + "stringValue": "Prevenar 20 vaccine suspension for injection 0.5ml pre-filled syringes", + "idValue": "40600011000001101", + "manufacturer": "Pfizer Ltd", + }, + ], + "BCG": [ + { + "system": "http://snomed.info/sct", + "code": "37240111000001101", + "display": "BCG Vaccine AJV powder for suspension for injection 1ml vials (AJ Vaccines)", + "stringValue": "BCG Vaccine AJV powder for suspension for injection 1ml vials", + "idValue": "37240111000001101", + "manufacturer": "AJ Vaccines", + }, + { + "system": "http://snomed.info/sct", + "code": "9316511000001100", + "display": "BCG vaccine powder and solvent for suspension for injection vials 10 vial (Pfizer Ltd)", + "stringValue": "BCG vaccine powder and solvent for suspension for injection vials 10 vial", + "idValue": "9316511000001100", + "manufacturer": "Pfizer Ltd", + }, + ], + "HEPB": [ + { + "system": "http://snomed.info/sct", + "code": "10752011000001102", + "display": "HBVAXPRO 10micrograms/1ml vaccine suspension for injection pre-filled syringes (Merck Sharp & Dohme (UK) Ltd)", + "stringValue": "HBVAXPRO 10micrograms/1ml vaccine suspension for injection pre-filled syringes", + "idValue": "107520110001102", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "871822003", + "display": "Vaccine product containing only Hepatitis B virus antigen (Pfizer Ltd)", + "stringValue": "Vaccine product containing only Hepatitis B virus antigen", + "idValue": "871822003", + "manufacturer": "Pfizer Ltd", + }, + ], + "HIB": [ + { + "system": "http://snomed.info/sct", + "code": "9903711000001109", + "display": "Menitorix powder and solvent for solution for injection 0.5ml vials (GlaxoSmithKline)", + "stringValue": "Haemophilus type b / Meningococcal C conjugate vaccine powder and solvent for solution for injection 0.5ml vials 1 vial", + "idValue": "99037110001109", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "9903611000001100", + "display": "Haemophilus type b / Meningococcal C conjugate vaccine powder and solvent for solution for injection 0.5ml vials 1 vial", + "stringValue": "Menitorix powder and solvent for solution for injection 0.5ml vials (GlaxoSmithKline)", + "idValue": "99036110001100", + "manufacturer": "Sanofi", + }, + ], + "MENB": [ + { + "system": "http://snomed.info/sct", + "code": "23584211000001109", + "display": "Bexsero vaccine suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "stringValue": "Bexsero vaccine powder and solvent for solution for injection 0.5ml vials", + "idValue": "235842110001109", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "37430711000001103", + "display": "Bexsero vaccine suspension for injection 0.5ml pre-filled syringes (CST Pharma Ltd) 1 pre-filled disposable injection", + "stringValue": "Bexsero vaccine suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "idValue": "374307110001103", + "manufacturer": "Pfizer Ltd", + }, + ], + "ROTAVIRUS": [ + { + "system": "http://snomed.info/sct", + "code": "34609911000001106", + "display": "Rotarix vaccine live oral suspension 1.5ml tube (GlaxoSmithKline UK Ltd)", + "stringValue": "Rotarix oral vaccine suspension for oral administration 1.5ml pre-filled syringes", + "idValue": "34609911000001106", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "17996111000001109", + "display": "Rotavirus vaccine live oral suspension 1.5ml pre-filled syringes", + "stringValue": "Rotarix vaccine live oral suspension 1.5ml tube (GlaxoSmithKline UK Ltd)", + "idValue": "17996111000001109", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd", + }, + ], + "4IN1": [ + { + "system": "http://snomed.info/sct", + "code": "26267211000001100", + "display": "Boostrix-IPV suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "stringValue": "Vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "871893003", + "display": "Vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "stringValue": "Boostrix-IPV suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "idValue": "1303503001", + "manufacturer": "Pfizer Ltd", + }, + ], + "6IN1": [ + { + "system": "http://snomed.info/sct", + "code": "34765811000001105", + "display": "Infanrix Hexa vaccine powder and suspension for suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd) ", + "stringValue": "Vaccine product containing only Clostridium tetani and Corynebacterium diphtheriae and inactivated Human poliovirus and acellular Bordetella pertussis and Haemophilus influenzae type b and Hepatitis B virus antigens", + "idValue": "347658110001105", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "1162634005", + "display": "Pediatric vaccine product containing only acellular Bordetella pertussis, Clostridium tetani and Corynebacterium diphtheriae toxoids, Haemophilus influenzae type b conjugated, Hepatitis B virus and inactivated Human poliovirus antigens", + "stringValue": "Infanrix Hexa vaccine powder and suspension for suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "idValue": "347658110001105", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + ], +} +VACCINATION_PROCEDURE_MAP = { + "COVID": [ + { + "system": "http://snomed.info/sct", + "code": "1362591000000103", + "display": "Immunisation course to maintain protection against SARS-CoV-2 (severe acute respiratory syndrome coronavirus 2)", + "stringValue": "Immunisation course to maintain protection against severe acute respiratory syndrome coronavirus 2 (regime/therapy)", + "idValue": "1362591000000103", + }, + { + "system": "http://snomed.info/sct", + "code": "1362591000000103", + "display": "Immunisation course to maintain protection against severe acute respiratory syndrome coronavirus 2 (regime/therapy)", + "stringValue": "Immunisation course to maintain protection against severe acute respiratory syndrome coronavirus 2", + "idValue": "1362591000000103", + }, + ], + "FLU": [ + { + "system": "http://snomed.info/sct", + "code": "884861000000100", + "display": "Seasonal influenza vaccination (procedure)", + "stringValue": "Administration of first intranasal seasonal influenza vaccination (procedure)", + "idValue": "884861000000100", + }, + { + "system": "http://snomed.info/sct", + "code": "884861000000100", + "display": "Administration of first intranasal seasonal influenza vaccination (procedure)", + "stringValue": "Seasonal influenza vaccination", + "idValue": "884861000000100", + }, + ], + "RSV": [ + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of RSV (respiratory syncytial virus) vaccine", + "stringValue": "Administration of respiratory syncytial virus vaccine", + "idValue": "1303503001", + }, + { + "system": "http://snomed.info/sct", + "code": "1303503001", + "display": "Administration of respiratory syncytial virus vaccine", + "stringValue": "Administration of vaccine product containing only Human orthopneumovirus antigen", + "idValue": "1303503001", + }, + ], + "HPV": [ + { + "system": "http://snomed.info/sct", + "code": "761841000", + "display": "Administration of vaccine product containing only Human papillomavirus antigen (procedure)", + "stringValue": "Administration of vaccine product containing only Human papillomavirus antigen", + "idValue": "761841000", + }, + { + "system": "http://snomed.info/sct", + "code": "761841000", + "display": "Administration of vaccine product containing only Human papillomavirus antigen (procedure)", + "stringValue": "Administration of vaccine product containing only Human papillomavirus antigen", + "idValue": "761841000", + }, + ], + "MENACWY": [ + { + "system": "http://snomed.info/sct", + "code": "871874000", + "display": "Administration of vaccine product containing only Neisseria meningitidis serogroup A, C, W135 and Y antigens (procedure)", + "stringValue": "Administration of vaccine product containing only Neisseria meningitidis serogroup ", + "idValue": "871874000", + }, + { + "system": "http://snomed.info/sct", + "code": "871874000", + "display": "Administration of vaccine product containing only Neisseria meningitidis serogroup A, C, W135 and Y antigens (procedure)", + "stringValue": "Administration of vaccine product containing only Neisseria meningitidis serogroup ", + "idValue": "871874000", + }, + ], + "MMR": [ + { + "system": "http://snomed.info/sct", + "code": "170433008", + "display": "Administration of second dose of vaccine product containing only Measles morbillivirus and Mumps orthorubulavirus and Rubella virus antigens", + "stringValue": "Administration of vaccine product containing only Measles virus and Mumps virus and Rubella virus antigens", + "idValue": "866186002", + }, + { + "system": "http://snomed.info/sct", + "code": "38598009", + "display": "Administration of vaccine product containing only Measles morbillivirus and Mumps orthorubulavirus and Rubella virus antigens", + "stringValue": "Administration of vaccine product containing only Measles virus and Mumps virus and Rubella virus antigens", + "idValue": "8666002", + }, + ], + "MMRV": [ + { + "system": "http://snomed.info/sct", + "code": "432636005", + "display": "Administration of vaccine product containing only Human alphaherpesvirus 3 and Measles morbillivirus and Mumps orthorubulavirus and Rubella virus antigens", + "stringValue": "vaccine product containing only Human alphaherpesvirus 3 and Measles morbillivirus and Mumps orthorubulavirus and Rubella virus antigens", + "idValue": "866182", + }, + { + "system": "http://snomed.info/sct", + "code": "433733003", + "display": "Administration of vaccine product containing only Human alphaherpesvirus 3 and Measles morbillivirus and Mumps orthorubulavirus and Rubella virus antigens", + "stringValue": "Administration of vaccine product containing only Human alphaherpesvirus 3 and Measles morbillivirus and Mumps orthorubulavirus and Rubella virus antigens", + "idValue": "86602", + }, + ], + "SHINGLES": [ + { + "system": "http://snomed.info/sct", + "code": "722215002", + "display": "Administration of vaccine product containing only Human alphaherpesvirus 3 antigen for shingles (procedure)", + "stringValue": "Administration of vaccine product containing only Human alphaherpesvirus 3 antigen for shingles", + "idValue": "4326365", + }, + { + "system": "http://snomed.info/sct", + "code": "1326111000000107", + "display": "Administration of second dose of vaccine product containing only Human alphaherpesvirus 3 antigen for shingles (procedure)", + "stringValue": "Administration of second dose of vaccine product containing only Human alphaherpesvirus 3 antigen for shingles", + "idValue": "432636512", + }, + ], + "3IN1": [ + { + "system": "http://snomed.info/sct", + "code": "414619005", + "display": "Administration of vaccine product containing only Clostridium tetani and low dose Corynebacterium diphtheriae and inactivated Human poliovirus antigens", + "stringValue": "Administration of vaccine product containing only Clostridium tetani and Corynebacterium diphtheriae and inactivated Human poliovirus antigens", + "idValue": "866182", + }, + { + "system": "http://snomed.info/sct", + "code": "866227002", + "display": "Administration of booster dose of vaccine product containing only Clostridium tetani and Corynebacterium diphtheriae and Human poliovirus antigens", + "stringValue": "Administration of vaccine product containing only Clostridium tetani and low dose Corynebacterium diphtheriae and inactivated Human poliovirus antigens", + "idValue": "8662002", + }, + ], + "PERTUSSIS": [ + { + "system": "http://snomed.info/sct", + "code": "956951000000104", + "display": "Pertussis vaccination in pregnancy (procedure)", + "stringValue": "Pertussis vaccination in pregnancy", + "idValue": "1000000104", + }, + ], + "PNEUMOCOCCAL": [ + { + "system": "http://snomed.info/sct", + "code": "722215002", + "display": "Administration of vaccine product containing only Human alphaherpesvirus 3 antigen for shingles (procedure)", + "stringValue": "Administration of vaccine product containing only Human alphaherpesvirus 3 antigen for shingles", + "idValue": "4326365", + }, + { + "system": "http://snomed.info/sct", + "code": "247631000000101", + "display": "First pneumococcal conjugated vaccination", + "stringValue": "First pneumococcal conjugated", + "idValue": "4326365", + }, + ], + "BCG": [ + { + "system": "http://snomed.info/sct", + "code": "42284007", + "display": "Administration of vaccine product containing only live attenuated Mycobacterium bovis antigen", + "stringValue": "Requires Bacillus Calmette-Guerin vaccination", + "idValue": "4326365", + }, + { + "system": "http://snomed.info/sct", + "code": "429069001", + "display": "Requires Bacillus Calmette-Guerin vaccination", + "stringValue": "Administration of vaccine product containing only live attenuated Mycobacterium bovis antigen", + "idValue": "4326365", + }, + ], + "HEPB": [ + { + "system": "http://snomed.info/sct", + "code": "170370000", + "display": "Administration of first dose of vaccine product containing only Hepatitis B virus antigen", + "stringValue": "Administration of booster dose of vaccine product containing only Hepatitis B virus antigen", + "idValue": "4326365", + }, + { + "system": "http://snomed.info/sct", + "code": "170373003", + "display": "Administration of booster dose of vaccine product containing only Hepatitis B virus antigen", + "stringValue": "Administration of first dose of vaccine product containing only Hepatitis B virus antigen", + "idValue": "4326365", + }, + ], + "HIB": [ + { + "system": "http://snomed.info/sct", + "code": "428975001", + "display": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "stringValue": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "idValue": "4326365", + }, + { + "system": "http://snomed.info/sct", + "code": "712833000", + "display": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "stringValue": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "idValue": "4326365", + }, + ], + "MENB": [ + { + "system": "http://snomed.info/sct", + "code": "720539004", + "display": "Administration of first dose of vaccine product containing only Neisseria meningitidis serogroup B antigen", + "stringValue": "Recombinant meningococcal group B and outer membrane vesicle vaccination", + "idValue": "235842110001109", + "manufacturer": "GlaxoSmithKline UK Ltd", + }, + { + "system": "http://snomed.info/sct", + "code": "516301000000101", + "display": "Recombinant meningococcal group B and outer membrane vesicle vaccination", + "stringValue": "Administration of first dose of vaccine product containing only Neisseria meningitidis serogroup B antigen", + "idValue": "516301000101", + "manufacturer": "CST Pharma Ltd", + }, + ], + "ROTAVIRUS": [ + { + "system": "http://snomed.info/sct", + "code": "868631000000102", + "display": "First rotavirus vaccination", + "stringValue": "Administration of vaccine product containing only Rotavirus antigen", + "idValue": "4326365", + }, + { + "system": "http://snomed.info/sct", + "code": "415354003", + "display": "Administration of vaccine product containing only Rotavirus antigen", + "stringValue": "First rotavirus vaccination", + "idValue": "4326365", + }, + ], + "4IN1": [ + { + "system": "http://snomed.info/sct", + "code": "868273007", + "display": "Administration of vaccine product containing only Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and Human poliovirus antigens", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001", + }, + { + "system": "http://snomed.info/sct", + "code": "247821000000102", + "display": "Booster diphtheria, tetanus, acellular pertussis and inactivated polio vaccination", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001", + }, + ], + "6IN1": [ + { + "system": "http://snomed.info/sct", + "code": "1082441000000108", + "display": "First diphtheria, tetanus and acellular pertussis, inactivated polio, Haemophilus influenzae type b and hepatitis B vaccination", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001", + }, + { + "system": "http://snomed.info/sct", + "code": "1162640003", + "display": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and Haemophilus influenzae type b and Hepatitis B virus and inactivated Human poliovirus antigens", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001", + }, + ], +} + +SITE_MAP = [ + { + "system": "http://snomed.info/sct", + "code": "368208006", + "display": "Left upper arm structure", + "idValue": "36820006", + "stringValue": "Left upper arm structure (body structure)", + }, + { + "system": "http://snomed.info/sct", + "code": "368209003", + "display": "Right upper arm structure", + "idValue": "36820903", + "stringValue": "Right upper arm structure (body structure)", + }, +] + + +ROUTE_MAP = [ + { + "system": "http://snomed.info/sct", + "code": "78421000", + "display": "Intramuscular", + "idValue": "7842100", + "stringValue": "Intramuscular route (qualifier value)", + }, + { + "system": "http://snomed.info/sct", + "code": "34206005", + "display": "Subcutaneous route (qualifier value)", + "idValue": "3420605", + "stringValue": "Subcutaneous route (qualifier value)", + }, +] + + +DOSE_QUANTITY_MAP = [ + { + "value": 0.3, + "unit": "Inhalation - unit of product usage", + "system": "http://snomed.info/sct", + "code": "2622896019", + } +] + +REASON_CODE_MAP = [ + {"system": "http://snomed.info/sct", "code": "443684005", "display": "Disease outbreak (event)"}, + {"system": "http://snomed.info/sct", "code": "310578008", "display": "Routine immunization schedule"}, +] + +PROTOCOL_DISEASE_MAP = { + "COVID": [ + { + "system": "http://snomed.info/sct", + "code": "840539006", + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2", + } + ], + "FLU": [{"system": "http://snomed.info/sct", "code": "6142004", "display": "Influenza"}], + "RSV": [ + {"system": "http://snomed.info/sct", "code": "55735004", "display": "Respiratory syncytial virus infection"} + ], + "HPV": [{"system": "http://snomed.info/sct", "code": "240532009", "display": "Human papilloma virus infection"}], + "MMR": [ + {"system": "http://snomed.info/sct", "code": "14189004", "display": "Measles"}, + {"system": "http://snomed.info/sct", "code": "36989005", "display": "Mumps"}, + {"system": "http://snomed.info/sct", "code": "36653000", "display": "Rubella"}, + ], + "MMRV": [ + {"system": "http://snomed.info/sct", "code": "14189004", "display": "Measles"}, + {"system": "http://snomed.info/sct", "code": "36989005", "display": "Mumps"}, + {"system": "http://snomed.info/sct", "code": "36653000", "display": "Rubella"}, + {"system": "http://snomed.info/sct", "code": "38907003", "display": "Varicella"}, + ], + "PERTUSSIS": [{"system": "http://snomed.info/sct", "code": "27836007", "display": "Pertussis"}], + "SHINGLES": [{"system": "http://snomed.info/sct", "code": "4740000", "display": "Herpes zoster"}], + "PNEUMOCOCCAL": [ + {"system": "http://snomed.info/sct", "code": "16814004", "display": "Pneumococcal infectious disease"} + ], + "3IN1": [ + {"system": "http://snomed.info/sct", "code": "398102009", "display": "Acute poliomyelitis"}, + { + "system": "http://snomed.info/sct", + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae", + }, + {"system": "http://snomed.info/sct", "code": "76902006", "display": "Tetanus"}, + ], + "MENACWY": [{"system": "http://snomed.info/sct", "code": "23511006", "display": "Meningococcal infectious disease"}], + "4IN1": [ + {"system": "http://snomed.info/sct", "code": "398102009", "display": "Acute poliomyelitis"}, + { + "system": "http://snomed.info/sct", + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae", + }, + {"system": "http://snomed.info/sct", "code": "27836007", "display": "Pertussis"}, + {"system": "http://snomed.info/sct", "code": "76902006", "display": "Tetanus"}, + ], + "6IN1": [ + {"system": "http://snomed.info/sct", "code": "398102009", "display": "Acute poliomyelitis"}, + { + "system": "http://snomed.info/sct", + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae", + }, + {"system": "http://snomed.info/sct", "code": "709410003", "display": "Haemophilus influenzae type b infection"}, + {"system": "http://snomed.info/sct", "code": "27836007", "display": "Pertussis"}, + {"system": "http://snomed.info/sct", "code": "76902006", "display": "Tetanus"}, + {"system": "http://snomed.info/sct", "code": "66071002", "display": "Type B viral hepatitis"}, + ], + "BCG": [{"system": "http://snomed.info/sct", "code": "56717001", "display": "Tuberculosis"}], + "HEPB": [{"system": "http://snomed.info/sct", "code": "66071002", "display": "Type B viral hepatitis"}], + "HIB": [ + {"system": "http://snomed.info/sct", "code": "709410003", "display": "Haemophilus influenzae type b infection"} + ], + "MENB": [ + { + "system": "http://snomed.info/sct", + "code": "1354584007", + "display": "Meningococcal infectious disease caused by Neisseria meningitidis serogroup B", + } + ], + "ROTAVIRUS": [{"system": "http://snomed.info/sct", "code": "186150001", "display": "Enteritis caused by rotavirus"}], +} diff --git a/tests/e2e_batch/Makefile b/tests/e2e_batch/Makefile deleted file mode 100644 index bd83837e83..0000000000 --- a/tests/e2e_batch/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -test: - poetry run python -m unittest -v -c - -.PHONY: test diff --git a/tests/e2e_batch/README.md b/tests/e2e_batch/README.md deleted file mode 100644 index 1d04151a00..0000000000 --- a/tests/e2e_batch/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# End-to-End Batch Test Suite (test_e2e_batch.py) - -This test suite provides automated end-to-end (E2E) testing for the Immunisation FHIR API batch processing pipeline. It verifies that batch file submissions are correctly processed, acknowledged, and validated across the system. - -## Overview - -- Framework: Python unittest -- Purpose: Simulate real-world batch file submissions, poll for acknowledgements, and validate processing results. -- Test Scenarios: Defined in the scenarios module and enabled in setUp(). -- Key Features: - - Uploads test batch files to S3. - - Waits for and validates ACK (acknowledgement) files. - - Cleans up SQS queues and test artifacts after each run. - -## Test Flow - -1. Setup (setUp) - -- Loads and enables a set of test scenarios. -- Prepares test data for batch submission. - -2. Test Execution (test_batch_submission) - -- Uploads ALL enabled test files to S3. -- Polls for ALL ACK responses and forwarded files. -- Validates the content and structure of ACK files. - -3. Teardown (tearDown) - -- Cleans up SQS queues and any generated test files. - -## Key Functions - -- send_files(tests): Uploads enabled test files to the S3 input bucket. -- poll_for_responses(tests, max_timeout): Polls for ACKs and processed files, with a timeout. -- validate_responses(tests): Validates the content of ACK files and checks for expected outcomes. - -## How to Run - -1. Ensure all dependencies and environment variables are set (see project root README). -2. Update `.env` file with contents indicated in `PR-NNN.env`, modified for PR -3. Update `.env` with reference to the appropriate AWS config profile `AWS_PROFILE={your-aws-profile}` -4. Update the apigee app to match the required PR-NNN -5. Run tests from vscode debugger or from makefile using - -``` -make test -``` diff --git a/tests/e2e_batch/clear_dynamodb.py b/tests/e2e_batch/clear_dynamodb.py deleted file mode 100644 index dc7d2fb426..0000000000 --- a/tests/e2e_batch/clear_dynamodb.py +++ /dev/null @@ -1,72 +0,0 @@ -import boto3 - -# Get the DynamoDB table name -TABLE_NAME = "imms-internal-dev-imms-events" - -if not TABLE_NAME: - raise ValueError("DYNAMODB_TABLE_NAME environment variable is not set") - -# Initialize DynamoDB client -dynamodb = boto3.resource("dynamodb") -table = dynamodb.Table(TABLE_NAME) - - -def get_primary_keys(): - """Retrieve the primary key schema of the table.""" - response = table.key_schema - return [key["AttributeName"] for key in response] - - -def get_total_count(): - """Get the total count of items in the table, handling pagination.""" - total_count = 0 - last_evaluated_key = None - - while True: - if last_evaluated_key: - response = table.scan(Select="COUNT", ExclusiveStartKey=last_evaluated_key) - else: - response = table.scan(Select="COUNT") - - total_count += response.get("Count", 0) - last_evaluated_key = response.get("LastEvaluatedKey") - - if not last_evaluated_key: - break - - return total_count - - -def clear_dynamodb(): - """Deletes all items from the DynamoDB table, handling pagination.""" - print(f"Clearing DynamoDB table: {TABLE_NAME}") - - primary_keys = get_primary_keys() - if not primary_keys: - raise ValueError("Unable to retrieve primary key schema") - - total_count = get_total_count() - print(f"Total items before deletion: {total_count}") - - deleted_count = 0 - - while True: - scan = table.scan() - items = scan.get("Items", []) - - if not items: - break - - with table.batch_writer() as batch: - for item in items: - key = {pk: item[pk] for pk in primary_keys} - batch.delete_item(Key=key) - deleted_count += 1 - - print(f"Deleted {len(items)} items...") - - print(f"Total {deleted_count} items deleted from DynamoDB") - - -if __name__ == "__main__": - clear_dynamodb() diff --git a/tests/e2e_batch/clients.py b/tests/e2e_batch/clients.py deleted file mode 100644 index 2411622e00..0000000000 --- a/tests/e2e_batch/clients.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Initialise clients, resources and logger. Note that all clients, resources and logger for the E2E BATCH -should be initialised ONCE ONLY (in this file) and then imported into the files where they are needed. -""" - -import logging - -from boto3 import client as boto3_client -from boto3 import resource as boto3_resource - -from constants import ( - REGION, - ack_metadata_queue_name, - audit_table_name, - batch_fifo_queue_name, - environment, -) - -# AWS Clients and Resources - - -s3_client = boto3_client("s3", region_name=REGION) - -dynamodb = boto3_resource("dynamodb", region_name=REGION) -sqs_client = boto3_client("sqs", region_name=REGION) -events_table_name = f"imms-{environment}-imms-events" -events_table = dynamodb.Table(events_table_name) -audit_table = dynamodb.Table(audit_table_name) -batch_fifo_queue_url = sqs_client.get_queue_url(QueueName=batch_fifo_queue_name)["QueueUrl"] -ack_metadata_queue_url = sqs_client.get_queue_url(QueueName=ack_metadata_queue_name)["QueueUrl"] -# Logger -logging.basicConfig(level="INFO") -logger = logging.getLogger() -logger.setLevel("INFO") diff --git a/tests/e2e_batch/constants.py b/tests/e2e_batch/constants.py deleted file mode 100644 index eb866e8e83..0000000000 --- a/tests/e2e_batch/constants.py +++ /dev/null @@ -1,100 +0,0 @@ -import os - -environment = os.environ.get("ENVIRONMENT", "internal-dev") -REGION = "eu-west-2" - -SOURCE_BUCKET = f"immunisation-batch-{environment}-data-sources" -INPUT_PREFIX = "" -ACK_BUCKET = f"immunisation-batch-{environment}-data-destinations" -FORWARDEDFILE_PREFIX = "forwardedFile/" -PRE_VALIDATION_ERROR = "Validation errors: doseQuantity.value must be a number" -POST_VALIDATION_ERROR = "Validation errors: contained[?(@.resourceType=='Patient')].name[0].given is a mandatory field" -DUPLICATE = "The provided identifier:" -ACK_PREFIX = "ack/" -TEMP_ACK_PREFIX = "TempAck/" -HEADER_RESPONSE_CODE_COLUMN = "HEADER_RESPONSE_CODE" -FILE_NAME_VAL_ERROR = "Infrastructure Level Response Value - Processing Error" -CONFIG_BUCKET = "imms-internal-dev-supplier-config" -PERMISSIONS_CONFIG_FILE_KEY = "permissions_config.json" -RAVS_URI = "https://www.ravs.england.nhs.uk/" -batch_fifo_queue_name = f"imms-{environment}-batch-file-created-queue.fifo" -ack_metadata_queue_name = f"imms-{environment}-ack-metadata-queue.fifo" -audit_table_name = f"immunisation-batch-{environment}-audit-table" - - -class EventName: - CREATE = "INSERT" - UPDATE = "MODIFY" - DELETE_LOGICAL = "MODIFY" - DELETE_PHYSICAL = "REMOVE" - - -class Operation: - CREATE = "CREATE" - UPDATE = "UPDATE" - DELETE_LOGICAL = "DELETE" - DELETE_PHYSICAL = "REMOVE" - - -class ActionFlag: - CREATE = "NEW" - UPDATE = "UPDATE" - DELETE_LOGICAL = "DELETE" - NONE = "NONE" - - -class InfResult: - SUCCESS = "Success" - PARTIAL_SUCCESS = "Partial Success" - FATAL_ERROR = "Fatal Error" - - -class BusRowResult: - SUCCESS = "OK" - FATAL_ERROR = "Fatal Error" - IMMS_NOT_FOUND = "Immunization resource does not exist" - NONE = "NONE" - - -class OperationOutcome: - IMMS_NOT_FOUND = "Immunization resource does not exist" - TEST = "TEST" - - -class OpMsgs: - VALIDATION_ERROR = "Validation errors" - MISSING_MANDATORY_FIELD = "is a mandatory field" - DOSE_QUANTITY_NOT_NUMBER = "doseQuantity.value must be a number" - IMM_NOT_EXIST = "Immunization resource does not exist" - IDENTIFIER_PROVIDED = "The provided identifier:" - INVALID_DATE_FORMAT = "is not in the correct format" - - -class DestinationType: - INF = ACK_PREFIX - BUS = FORWARDEDFILE_PREFIX - - -class ActionSequence: - def __init__(self, desc: str, actions: list[ActionFlag], outcome: ActionFlag = None): - self.actions = actions - self.description = desc - self.outcome = outcome if outcome else actions[-1] - - -class PermPair: - def __init__(self, ods_code: str, permissions: str): - self.ods_code = ods_code - self.permissions = permissions - - -class TestSet: - CREATE_OK = ActionSequence("Create. OK", [ActionFlag.CREATE]) - UPDATE_OK = ActionSequence("Update. OK", [ActionFlag.CREATE, ActionFlag.UPDATE]) - DELETE_OK = ActionSequence("Delete. OK", [ActionFlag.CREATE, ActionFlag.UPDATE, ActionFlag.DELETE_LOGICAL]) - REINSTATE_OK = ActionSequence( - "Reinstate. OK", - [ActionFlag.CREATE, ActionFlag.DELETE_LOGICAL, ActionFlag.UPDATE], - ) - DELETE_FAIL = ActionSequence("Delete without Create. Fail", [ActionFlag.DELETE_LOGICAL]) - UPDATE_FAIL = ActionSequence("Update without Create. Fail", [ActionFlag.UPDATE], outcome=ActionFlag.NONE) diff --git a/tests/e2e_batch/errors.py b/tests/e2e_batch/errors.py deleted file mode 100644 index 794253b0b9..0000000000 --- a/tests/e2e_batch/errors.py +++ /dev/null @@ -1,6 +0,0 @@ -class AckFileNotFoundError(Exception): - """Raised when the acknowledgment file is not found.""" - - -class DynamoDBMismatchError(Exception): - """Raised when DynamoDB primary key doesn't match the ACK file IMMS_ID.""" diff --git a/tests/e2e_batch/poetry.lock b/tests/e2e_batch/poetry.lock deleted file mode 100644 index 3828227432..0000000000 --- a/tests/e2e_batch/poetry.lock +++ /dev/null @@ -1,323 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. - -[[package]] -name = "boto3" -version = "1.41.5" -description = "The AWS SDK for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "boto3-1.41.5-py3-none-any.whl", hash = "sha256:bb278111bfb4c33dca8342bda49c9db7685e43debbfa00cc2a5eb854dd54b745"}, - {file = "boto3-1.41.5.tar.gz", hash = "sha256:bc7806bee681dfdff2fe2b74967b107a56274f1e66ebe4d20dc8eee1ea408d17"}, -] - -[package.dependencies] -botocore = ">=1.41.5,<1.42.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.15.0,<0.16.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.41.6" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "botocore-1.41.6-py3-none-any.whl", hash = "sha256:963cc946e885acb941c96e7d343cb6507b479812ca22566ceb3e9410d0588de0"}, - {file = "botocore-1.41.6.tar.gz", hash = "sha256:08fe47e9b306f4436f5eaf6a02cb6d55c7745d13d2d093ce5d917d3ef3d3df75"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} - -[package.extras] -crt = ["awscrt (==0.29.1)"] - -[[package]] -name = "jmespath" -version = "1.0.1" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "numpy" -version = "2.4.1" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5"}, - {file = "numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425"}, - {file = "numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba"}, - {file = "numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501"}, - {file = "numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a"}, - {file = "numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509"}, - {file = "numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc"}, - {file = "numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82"}, - {file = "numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0"}, - {file = "numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574"}, - {file = "numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a"}, - {file = "numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0"}, - {file = "numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c"}, - {file = "numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02"}, - {file = "numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162"}, - {file = "numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9"}, - {file = "numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f"}, - {file = "numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87"}, - {file = "numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9"}, - {file = "numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e"}, - {file = "numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5"}, - {file = "numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8"}, - {file = "numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c"}, - {file = "numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2"}, - {file = "numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d"}, - {file = "numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb"}, - {file = "numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5"}, - {file = "numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7"}, - {file = "numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d"}, - {file = "numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15"}, - {file = "numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9"}, - {file = "numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2"}, - {file = "numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505"}, - {file = "numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2"}, - {file = "numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4"}, - {file = "numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510"}, - {file = "numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220"}, - {file = "numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee"}, - {file = "numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556"}, - {file = "numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844"}, - {file = "numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3"}, - {file = "numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205"}, - {file = "numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745"}, - {file = "numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d"}, - {file = "numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df"}, - {file = "numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f"}, - {file = "numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0"}, - {file = "numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c"}, - {file = "numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93"}, - {file = "numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42"}, - {file = "numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01"}, - {file = "numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b"}, - {file = "numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a"}, - {file = "numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2"}, - {file = "numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be"}, - {file = "numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33"}, - {file = "numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690"}, -] - -[[package]] -name = "pandas" -version = "2.3.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, - {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, - {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, - {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, - {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, - {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, - {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, - {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, - {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, - {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, - {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, - {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, - {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, - {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, - {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, - {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, - {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, - {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, - {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, - {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, - {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, - {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, - {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, - {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, - {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, - {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, - {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, - {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, - {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, - {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, - {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, - {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, - {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, - {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, - {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, - {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, - {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, - {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, - {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, - {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, - {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, - {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, - {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, - {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, - {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, - {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, - {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, - {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, - {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, - {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, - {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, - {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, - {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, - {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, - {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, -] - -[package.dependencies] -numpy = {version = ">=1.23.2", markers = "python_version == \"3.11\""} -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2025.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, -] - -[[package]] -name = "s3transfer" -version = "0.15.0" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "s3transfer-0.15.0-py3-none-any.whl", hash = "sha256:6f8bf5caa31a0865c4081186689db1b2534cef721d104eb26101de4b9d6a5852"}, - {file = "s3transfer-0.15.0.tar.gz", hash = "sha256:d36fac8d0e3603eff9b5bfa4282c7ce6feb0301a633566153cbd0b93d11d8379"}, -] - -[package.dependencies] -botocore = ">=1.37.4,<2.0a.0" - -[package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "tzdata" -version = "2025.3" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -files = [ - {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, - {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[metadata] -lock-version = "2.1" -python-versions = "~3.11" -content-hash = "805001fc3b637c8bb3cbe67dadf9ba05a4626ff74616f092279327e3489bd8d2" diff --git a/tests/e2e_batch/pr-NNN.env b/tests/e2e_batch/pr-NNN.env deleted file mode 100644 index 7b9ddf010a..0000000000 --- a/tests/e2e_batch/pr-NNN.env +++ /dev/null @@ -1,2 +0,0 @@ -ENVIRONMENT=pr-NNN -AWS_PROFILE={your-aws-profile} diff --git a/tests/e2e_batch/pyproject.toml b/tests/e2e_batch/pyproject.toml deleted file mode 100644 index 8b553dd613..0000000000 --- a/tests/e2e_batch/pyproject.toml +++ /dev/null @@ -1,12 +0,0 @@ -[tool.poetry] -name = "e2e_batch" -version = "0.1.0" -description = "End-to-end tests for immunization-batch" -authors = ["Your Name "] -license = "MIT" -readme = "README.md" - -[tool.poetry.dependencies] -python = "~3.11" -boto3 = "~1.41.0" -pandas = "^2.3.3" diff --git a/tests/e2e_batch/scenarios.py b/tests/e2e_batch/scenarios.py deleted file mode 100644 index 380d0e4b66..0000000000 --- a/tests/e2e_batch/scenarios.py +++ /dev/null @@ -1,238 +0,0 @@ -import csv -import uuid -from datetime import datetime, timezone - -import pandas as pd -from clients import logger -from errors import DynamoDBMismatchError -from vax_suppliers import OdsVax, TestPair - -from constants import ( - ACK_BUCKET, - RAVS_URI, - ActionFlag, - BusRowResult, - DestinationType, - Operation, - OperationOutcome, -) -from utils import ( - aws_cleanup, - create_row, - fetch_pk_and_operation_from_dynamodb, - get_file_content_from_s3, - poll_s3_file_pattern, - validate_fatal_error, -) - - -class TestAction: - def __init__( - self, - action: ActionFlag, - expected_header_response_code=BusRowResult.SUCCESS, - expected_operation_outcome="", - ): - self.action = action - self.expected_header_response_code = expected_header_response_code - self.expected_operation_outcome = expected_operation_outcome - - -class TestCase: - def __init__(self, scenario: dict): - self.name: str = scenario.get("name", "Unnamed Test Case") - self.description: str = scenario.get("description", "") - self.is_failure_scenario = scenario.get("is_failure_scenario", False) - self.ods_vax: OdsVax = scenario.get("ods_vax") - self.actions: list[TestAction] = scenario.get("actions", []) - self.ods = self.ods_vax.ods_code - self.vax = self.ods_vax.vax - self.dose_amount: float = scenario.get("dose_amount", 0.5) - self.inject_cp1252 = scenario.get("create_with_cp1252_encoded_character", False) - self.header = scenario.get("header", "NHS_NUMBER") - self.version = scenario.get("version", 5) - self.operation_outcome = scenario.get("operation_outcome", "") - self.enabled = scenario.get("enabled", False) - self.ack_keys = {DestinationType.INF: None, DestinationType.BUS: None} - # initialise attribs to be set later - self.key = None # S3 key of the uploaded file - self.file_name = None # name of the generated CSV file - self.identifier = None # unique identifier of subject in the CSV file rows - - def get_poll_destinations(self, pending: bool) -> bool: - # loop through keys in test (inf and bus) - for ack_key in self.ack_keys.keys(): - if not self.ack_keys[ack_key]: - found_ack_key = self.poll_destination(ack_key) - if found_ack_key: - self.ack_keys[ack_key] = found_ack_key - else: - pending = True - return pending - - def poll_destination(self, ack_prefix: DestinationType): - """Poll the ACK_BUCKET for an ack file that contains the input_file_name as a substring.""" - input_file_name = self.file_name - filename_without_ext = input_file_name[:-4] if input_file_name.endswith(".csv") else input_file_name - search_pattern = f"{ack_prefix}{filename_without_ext}" - return poll_s3_file_pattern(ack_prefix, search_pattern) - - def check_final_success_action(self): - desc = f"{self.name} - outcome" - outcome = self.operation_outcome - dynamo_pk, operation, is_reinstate = fetch_pk_and_operation_from_dynamodb(self.get_identifier_pk()) - - expected_operation = Operation.CREATE if outcome == ActionFlag.CREATE else outcome - if operation != expected_operation: - raise DynamoDBMismatchError( - ( - f"{desc}. Final Event Table Operation: Mismatch - DynamoDB Operation '{operation}' " - f"does not match operation requested '{outcome}' (3)" - ) - ) - - def get_identifier_pk(self): - if not self.identifier: - raise Exception("Identifier not set. Generate the CSV file first.") - return f"{RAVS_URI}#{self.identifier}" - - def check_bus_file_content(self): - desc = f"{self.name} - bus" - content = get_file_content_from_s3(ACK_BUCKET, self.ack_keys[DestinationType.BUS]) - reader = csv.DictReader(content.splitlines(), delimiter="|") - rows = list(reader) - - for i, row in enumerate(rows): - response_code = self.actions[i].expected_header_response_code - operation_outcome = self.actions[i].expected_operation_outcome - if response_code and "HEADER_RESPONSE_CODE" in row: - row_HEADER_RESPONSE_CODE = row["HEADER_RESPONSE_CODE"].strip() - assert row_HEADER_RESPONSE_CODE == response_code, ( - f"{desc}.Row {i} expected HEADER_RESPONSE_CODE '{response_code}', " - f"but got '{row_HEADER_RESPONSE_CODE}'" - ) - if operation_outcome and "OPERATION_OUTCOME" in row: - row_OPERATION_OUTCOME = row["OPERATION_OUTCOME"].strip() - assert row_OPERATION_OUTCOME.startswith(operation_outcome), ( - f"{desc}.Row {i} expected OPERATION_OUTCOME '{operation_outcome}', but got '{row_OPERATION_OUTCOME}'" - ) - elif row_HEADER_RESPONSE_CODE == "Fatal Error": - validate_fatal_error(desc, row, i, operation_outcome) - - def generate_csv_file(self): - self.file_name = self.get_file_name(self.vax, self.ods, self.version) - logger.info(f'Test "{self.name}" File {self.file_name}') - data = [] - self.identifier = str(uuid.uuid4()) - for action in self.actions: - row = create_row( - self.identifier, - self.dose_amount, - action.action, - self.header, - self.inject_cp1252, - ) - logger.info(f" > {action.action} - {self.vax}/{self.ods} - {self.identifier}") - data.append(row) - df = pd.DataFrame(data) - - df.to_csv(self.file_name, index=False, sep="|", quoting=csv.QUOTE_MINIMAL) - - def get_file_name(self, vax_type, ods, version="5"): - timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S00") - return f"{vax_type}_Vaccinations_v{version}_{ods}_{timestamp}.csv" - - def cleanup(self): - aws_cleanup(self.key, self.identifier, self.ack_keys) - - -def create_test_cases(test_case_dict: dict) -> list[TestCase]: - """Initialize test cases from a dictionary.""" - return [TestCase(name) for name in test_case_dict] - - -def enable_tests(test_cases: list[TestCase], names: list[str]) -> None: - """Enable only the test cases with the given names.""" - for name in names: - for test in test_cases: - if test.name == name: - test.enabled = True - break - else: - raise Exception(f"Test case with name '{name}' not found.") - - -def generate_csv_files(test_cases: list[TestCase]) -> list[TestCase]: - """Generate CSV files for all enabled test cases.""" - ret = [] - for test in test_cases: - if test.enabled: - test.generate_csv_file() - ret.append(test) - - -scenarios = { - "dev": [ - { - "name": "Successful Create", - "ods_vax": TestPair.E8HA94_COVID_CUD, - "operation_outcome": ActionFlag.CREATE, - "actions": [TestAction(ActionFlag.CREATE)], - "description": "Successful Create", - }, - { - "name": "Successful Update", - "description": "Successful Create,Update", - "ods_vax": TestPair.DPSFULL_COVID_CRUDS, - "operation_outcome": ActionFlag.UPDATE, - "actions": [TestAction(ActionFlag.CREATE), TestAction(ActionFlag.UPDATE)], - }, - { - "name": "Successful Delete", - "description": "Successful Create,Update, Delete", - "ods_vax": TestPair.V0V8L_FLU_CRUDS, - "operation_outcome": ActionFlag.DELETE_LOGICAL, - "actions": [ - TestAction(ActionFlag.CREATE), - TestAction(ActionFlag.DELETE_LOGICAL), - ], - }, - { - "name": "Failed Update", - "description": "Failed Update - resource does not exist", - "ods_vax": TestPair.V0V8L_HPV_CUD, - "actions": [ - TestAction( - ActionFlag.UPDATE, - expected_header_response_code=BusRowResult.FATAL_ERROR, - expected_operation_outcome=OperationOutcome.IMMS_NOT_FOUND, - ) - ], - "is_failure_scenario": True, - "operation_outcome": ActionFlag.NONE, - }, - { - "name": "Failed Delete", - "description": "Failed Delete - resource does not exist", - "ods_vax": TestPair.X26_MMR_CRUDS, - "actions": [ - TestAction( - ActionFlag.DELETE_LOGICAL, - expected_header_response_code=BusRowResult.FATAL_ERROR, - expected_operation_outcome=OperationOutcome.IMMS_NOT_FOUND, - ) - ], - "is_failure_scenario": True, - "operation_outcome": ActionFlag.NONE, - }, - { - "name": "Create with 1252 char", - "description": "Create with 1252 char", - "ods_vax": TestPair.YGA_MENACWY_CRUDS, - "operation_outcome": ActionFlag.CREATE, - "actions": [TestAction(ActionFlag.CREATE)], - "create_with_cp1252_encoded_character": True, - }, - ], - "ref": [], -} diff --git a/tests/e2e_batch/test_e2e_batch.py b/tests/e2e_batch/test_e2e_batch.py deleted file mode 100644 index 7eee66b6cb..0000000000 --- a/tests/e2e_batch/test_e2e_batch.py +++ /dev/null @@ -1,145 +0,0 @@ -import time -import unittest - -from clients import logger -from scenarios import ( - TestCase, - create_test_cases, - enable_tests, - generate_csv_files, - scenarios, -) - -from constants import ( - ACK_BUCKET, - INPUT_PREFIX, - SOURCE_BUCKET, - TEMP_ACK_PREFIX, - DestinationType, - environment, -) -from utils import ( - check_ack_file_content, - delete_file_from_s3, - get_file_content_from_s3, - purge_sqs_queues, - upload_file_to_s3, - validate_row_count, -) - - -# For consideration - these tests are suboptimal and difficult to read and extend. Consider integrating the automation -# test repo into this one in future (it currently lives elsewhere but uses a nice readable framework to drive the tests) -class TestE2EBatch(unittest.TestCase): - def setUp(self): - self.tests: list[TestCase] = create_test_cases(scenarios["dev"]) - enable_tests( - self.tests, - [ - "Successful Create", - "Successful Update", - "Successful Delete", - "Create with 1252 char", - "Failed Update", - "Failed Delete", - ], - ) - generate_csv_files(self.tests) - - def tearDown(self): - logger.info("Cleanup...") - for test in self.tests: - test.cleanup() - delete_file_from_s3(ACK_BUCKET, TEMP_ACK_PREFIX) - purge_sqs_queues() - - @unittest.skipIf(environment == "ref", "Skip for ref") - def test_batch_submission(self): - """Test all scenarios and submit as batch.""" - start_time = time.time() - max_timeout = 600 # seconds - - send_files(self.tests) - - if not poll_for_responses(self.tests, max_timeout): - logger.error("Timeout waiting for responses") - - validate_responses(self.tests) - - logger.info(f"Tests Completed. Time: {time.time() - start_time:.1f} seconds") - - -def send_files(tests: list[TestCase]): - start_time = time.time() - for test in tests: - if test.enabled: - logger.info(f"Upload {test.file_name} ") - key = upload_file_to_s3(test.file_name, SOURCE_BUCKET, INPUT_PREFIX) - test.key = key - logger.info(f"Files uploaded. Time: {time.time() - start_time:.1f} seconds") - - -def poll_for_responses(tests: list[TestCase], max_timeout=1200) -> bool: - logger.info("Waiting while processing...") - start_time = time.time() - # while there are still pending files, poll for acks and forwarded files - pending = True - while pending: - pending = False - for test in tests: - pending = test.get_poll_destinations(pending) - if pending: - print(".", end="") - time.sleep(5) - if (time.time() - start_time) > max_timeout: - return False - logger.info(f"Files processed. Time: {time.time() - start_time:.1f} seconds") - return True - - -def validate_responses(tests: list[TestCase]): - start_time = time.time() - current_ack_file_count = 0 - - # We expect an INF Ack (high-level i.e. file accepted or not) and a BUS Ack (containing all failure rows) for each - # batch file - expected_ack_file_count = len(tests) * 2 - errors = False - try: - for test in tests: - logger.info(f"Validation for Test: {test.name} ") - # Validate the ACK file - if test.ack_keys[DestinationType.INF]: - current_ack_file_count += 1 - inf_ack_content = get_file_content_from_s3(ACK_BUCKET, test.ack_keys[DestinationType.INF]) - check_ack_file_content(test.name, inf_ack_content, "Success", None, test.operation_outcome) - else: - logger.error(f"INF ACK file not found for test: {test.name}") - errors = True - - if test.ack_keys[DestinationType.BUS]: - current_ack_file_count += 1 - validate_row_count( - f"{test.name} - bus", test.file_name, test.ack_keys[DestinationType.BUS], test.is_failure_scenario - ) - - test.check_bus_file_content() - - test.check_final_success_action() - else: - logger.error(f"BUS ACK file not found for test: {test.name}") - errors = True - - except Exception as e: - logger.error(f"Error during validation: {e}") - errors = True - finally: - if current_ack_file_count == expected_ack_file_count: - logger.info("All responses subject to validation.") - else: - logger.error(f"{current_ack_file_count} of {expected_ack_file_count} responses subject to validation.") - logger.info(f"Time: {time.time() - start_time:.1f} seconds") - assert current_ack_file_count == expected_ack_file_count, ( - f"Only {current_ack_file_count} of {expected_ack_file_count} responses subject to validation." - ) - assert not errors, "Errors found during validation." diff --git a/tests/e2e_batch/utils.py b/tests/e2e_batch/utils.py deleted file mode 100644 index c3cc5c4adb..0000000000 --- a/tests/e2e_batch/utils.py +++ /dev/null @@ -1,551 +0,0 @@ -import csv -import io -import json -import os -import random -import time -import uuid -from datetime import datetime, timezone -from io import StringIO - -import pandas as pd -from boto3.dynamodb.conditions import Key -from botocore.exceptions import ClientError -from clients import ( - ack_metadata_queue_url, - audit_table, - batch_fifo_queue_url, - events_table, - logger, - s3_client, - sqs_client, -) -from errors import AckFileNotFoundError, DynamoDBMismatchError - -from constants import ( - ACK_BUCKET, - ACK_PREFIX, - DUPLICATE, - FILE_NAME_VAL_ERROR, - FORWARDEDFILE_PREFIX, - HEADER_RESPONSE_CODE_COLUMN, - RAVS_URI, - SOURCE_BUCKET, - ActionFlag, - environment, -) - - -def upload_file_to_s3(file_name, bucket, prefix): - """Upload the given file to the specified bucket under the provided prefix. - Returns the S3 key if successful, or raises an exception.""" - - key = f"{prefix}{file_name}" - try: - with open(file_name, "rb") as f: - response = s3_client.put_object(Bucket=bucket, Key=key, Body=f) - - # Confirm success - status_code = response.get("ResponseMetadata", {}).get("HTTPStatusCode") - if status_code != 200: - raise Exception(f"Upload failed with status code: {status_code}") - - os.remove(file_name) - return key - - except ClientError as e: - raise Exception(f"ClientError during S3 upload: {e}") - except Exception as e: - raise Exception(f"Unexpected error during file upload: {e}") - - -def delete_file_from_s3(bucket, key): - """Delete the specified file (object) from the given S3 bucket. - Returns True if deletion is successful, otherwise raises an exception.""" - try: - if key and key.strip(): - response = s3_client.delete_object(Bucket=bucket, Key=key) - - # Optionally verify deletion status - status_code = response.get("ResponseMetadata", {}).get("HTTPStatusCode") - if status_code != 204: - raise Exception(f"Delete failed with status code: {status_code}") - - print(f"Deleted {key}") - return True - - except ClientError as e: - raise Exception(f"ClientError during S3 delete: {e}") - except Exception as e: - raise Exception(f"Unexpected error during file deletion: {e}") - - -def wait_for_ack_file(ack_prefix, input_file_name, timeout=600): - """Poll the ACK_BUCKET for an ack file that contains the input_file_name as a substring.""" - - filename_without_ext = input_file_name[:-4] if input_file_name.endswith(".csv") else input_file_name - if ack_prefix: - search_pattern = f"{ACK_PREFIX}{filename_without_ext}" - ack_prefix = ACK_PREFIX - else: - search_pattern = f"{FORWARDEDFILE_PREFIX}{filename_without_ext}" - ack_prefix = FORWARDEDFILE_PREFIX - start_time = time.time() - while time.time() - start_time < timeout: - response = s3_client.list_objects_v2(Bucket=ACK_BUCKET, Prefix=ack_prefix) - if "Contents" in response: - for obj in response["Contents"]: - key = obj["Key"] - if search_pattern in key: - return key - time.sleep(5) - raise AckFileNotFoundError( - f"Ack file matching '{search_pattern}' not found in bucket {ACK_BUCKET} within {timeout} seconds." - ) - - -def get_file_content_from_s3(bucket, key): - """Download and return the file content from S3.""" - - response = s3_client.get_object(Bucket=bucket, Key=key) - content = response["Body"].read().decode("utf-8") - return content - - -def check_ack_file_content(desc, content, response_code, operation_outcome, operation_requested) -> bool: - """ - Parse and validate the acknowledgment (ACK) CSV file content. - - The function reads the content of an ACK CSV file using a pipe '|' delimiter, - then verifies the number of rows and their content based on the provided response_code, - operation_outcome, and operation_requested. - - Scenarios: - - DUPLICATE scenario: If `operation_outcome` contains "The provided identifier:", - expect exactly two rows: - - First row: HEADER_RESPONSE_CODE = "OK" with a valid operation requested. - - Second row: HEADER_RESPONSE_CODE = "Fatal Error" with the identifier message. - - - Normal scenarios: For each row: - - Verify HEADER_RESPONSE_CODE matches `response_code`. - - Verify OPERATION_OUTCOME matches `operation_outcome`. - - Validate row content based on HEADER_RESPONSE_CODE: - - "OK" calls `validate_ok_response`. - - "Fatal Error" calls `validate_fatal_error`. - - Args: - content (str): The CSV file content as a string. - response_code (str): Expected response code (e.g., "OK" or "Fatal Error"). - operation_outcome (str): Expected operation outcome message. - operation_requested (str): The requested operation to validate in successful rows. - - Raises: - AssertionError: If the row count, HEADER_RESPONSE_CODE, or OPERATION_OUTCOME is incorrect. - """ - - reader = csv.DictReader(content.splitlines(), delimiter="|") - rows = list(reader) - - if operation_outcome and DUPLICATE in operation_outcome: - # Handle DUPLICATE scenario: - assert len(rows) == 2, f"{desc}. Expected 2 rows for DUPLICATE scenario, got {len(rows)}" - - first_row = rows[0] - validate_header_response_code(desc, first_row, 0, "OK") - validate_ok_response(first_row, 0, operation_requested) - - second_row = rows[1] - validate_header_response_code(desc, second_row, 1, "Fatal Error") - validate_fatal_error(desc, second_row, 1, DUPLICATE) - else: - # Handle normal scenarios: - for i, row in enumerate(rows): - if response_code and "HEADER_RESPONSE_CODE" in row: - row_HEADER_RESPONSE_CODE = row["HEADER_RESPONSE_CODE"].strip() - assert row_HEADER_RESPONSE_CODE == response_code, ( - f"{desc}.Row {i} expected HEADER_RESPONSE_CODE '{response_code}', " - f"but got '{row_HEADER_RESPONSE_CODE}'" - ) - if operation_outcome and "OPERATION_OUTCOME" in row: - assert row["OPERATION_OUTCOME"].strip() == operation_outcome, ( - f"Row {i} expected OPERATION_OUTCOME '{operation_outcome}', " - f"but got '{row['OPERATION_OUTCOME'].strip()}'" - ) - if row["HEADER_RESPONSE_CODE"].strip() == "OK": - validate_ok_response(row, i, operation_requested) - elif row["HEADER_RESPONSE_CODE"].strip() == "Fatal Error": - validate_fatal_error(row, i, operation_outcome) - - -def validate_header_response_code(desc, row, index, expected_code): - """Ensure HEADER_RESPONSE_CODE exists and matches expected response code.""" - - if "HEADER_RESPONSE_CODE" not in row: - raise ValueError(f"Row {index + 1} does not have a 'HEADER_RESPONSE_CODE' column.") - if row["HEADER_RESPONSE_CODE"].strip() != expected_code: - raise ValueError( - f"Row {index + 1}: Expected RESPONSE '{expected_code}', but found '{row['HEADER_RESPONSE_CODE']}'" - ) - - -def validate_fatal_error(row, index, expected_outcome): - """Ensure OPERATION_OUTCOME matches expected outcome for Fatal Error responses.""" - - if FILE_NAME_VAL_ERROR in expected_outcome: - if expected_outcome not in row["RESPONSE_DISPLAY"].strip(): - raise ValueError( - f"Row {index + 1}: Expected RESPONSE '{expected_outcome}', but found '{row['RESPONSE_DISPLAY']}'" - ) - - if expected_outcome not in row["OPERATION_OUTCOME"].strip(): - raise ValueError( - f"Row {index + 1}: Expected RESPONSE '{expected_outcome}', but found '{row['OPERATION_OUTCOME']}'" - ) - - -def validate_ok_response(row, index, operation_requested): - """ - Validate the LOCAL_ID format and verify that the DynamoDB primary key (PK) - and operation match the expected values for OK responses. - - This function extracts the identifier PK from the given row, fetches the PK and - operation from DynamoDB, and compares them against the row's IMMS_ID and the - requested operation. If the operation is 'reinstated', additional validation - ensures that the DynamoDB operation is 'UPDATE' and marked as reinstated. - - Args: - row (dict): A dictionary representing a single row of the ACK file. - index (int): The zero-based index of the row in the ACK file (for error messages). - operation_requested (str): The expected operation (e.g., 'CREATE', 'UPDATE', 'reinstated'). - - Raises: - ValueError: If the 'LOCAL_ID' column is missing in the row. - DynamoDBMismatchError: If the DynamoDB PK, operation, or reinstatement status does not match. - """ - - if "LOCAL_ID" not in row: - raise ValueError(f"Row {index + 1} does not have a 'LOCAL_ID' column.") - identifier_pk = extract_identifier_pk(row, index) - dynamo_pk, operation, is_reinstate = fetch_pk_and_operation_from_dynamodb(identifier_pk) - if dynamo_pk != row["IMMS_ID"]: - raise DynamoDBMismatchError( - f"Row {index + 1}: Mismatch - DynamoDB PK '{dynamo_pk}' does not match ACK file IMMS_ID '{row['IMMS_ID']}'" - ) - - if operation_requested == "reinstated" or operation_requested == "update-reinstated": - if operation != "UPDATE": - raise DynamoDBMismatchError( - ( - f"Row {index + 1}: Mismatch - DynamoDB Operation '{operation}' " - f"does not match operation requested '{operation_requested}'" - ) - ) - if is_reinstate != "reinstated": - raise DynamoDBMismatchError( - ( - f"Row {index + 1}: Mismatch - DynamoDB Operation '{is_reinstate}' " - f"does not match operation requested 'reinstated'" - ) - ) - elif operation != operation_requested: - raise DynamoDBMismatchError( - ( - f"Row {index + 1}: Mismatch - DynamoDB Operation '{operation}' " - f"does not match operation requested '{operation_requested}'" - ) - ) - - -def extract_identifier_pk(row, index): - """Extract LOCAL_ID and convert to IdentifierPK.""" - try: - local_id, unique_id_uri = row["LOCAL_ID"].split("^") - return f"{unique_id_uri}#{local_id}" - except ValueError: - raise AssertionError(f"Row {index + 1}: Invalid LOCAL_ID format - {row['LOCAL_ID']}") - - -def fetch_pk_and_operation_from_dynamodb(identifier_pk): - """ - Fetch the primary key (PK) and operation from DynamoDB using the provided IdentifierPK. - - This function queries the DynamoDB table using the 'IdentifierGSI' index to find the - item associated with the given identifier_pk. If the item is found, it returns the PK, - operation, and DeletedAt (if present). Otherwise, it returns 'NOT_FOUND'. If an error - occurs during the query, it logs the error and returns 'ERROR'. - - Args: - identifier_pk (str): The identifier key used to query DynamoDB. - - Returns: - tuple or str: A tuple containing (PK, Operation, DeletedAt) if found, - 'NOT_FOUND' if no item is found, or 'ERROR' if an exception occurs. - - Raises: - Logs any exceptions encountered during the DynamoDB query. - """ - try: - response = events_table.query( - IndexName="IdentifierGSI", - KeyConditionExpression="IdentifierPK = :identifier_pk", - ExpressionAttributeValues={":identifier_pk": identifier_pk}, - ) - if "Items" in response: - items = response["Items"] - if items: - if "DeletedAt" in items[0]: - return ( - items[0]["PK"], - items[0]["Operation"], - items[0]["DeletedAt"], - ) - return (items[0]["PK"], items[0]["Operation"], None) - return (identifier_pk, ActionFlag.NONE, None) - - except Exception as e: - logger.error(f"Error fetching from DynamoDB: {e}") - return "ERROR" - - -def validate_row_count(desc: str, source_file_name: str, ack_file_name: str, is_failure_scenario: bool) -> None: - """ - Compare the row count of a file in one S3 bucket with a file in another S3 bucket. - Raises: - AssertionError: If the row counts do not match. - """ - # The BUS Ack will only add failed rows to the final file. - source_file_row_count = fetch_row_count(SOURCE_BUCKET, f"archive/{source_file_name}") if is_failure_scenario else 1 - ack_file_row_count = fetch_row_count(ACK_BUCKET, ack_file_name) - - assert source_file_row_count == ack_file_row_count, ( - f"{desc}. Row count mismatch: Input ({source_file_row_count}) vs Ack ({ack_file_row_count})" - ) - - -def fetch_row_count(bucket, file_name): - "Fetch the row count for the file from the s3 bucket" - - response_input = s3_client.get_object(Bucket=bucket, Key=file_name) - content_input = response_input["Body"].read().decode("utf-8") - return sum(1 for _ in csv.reader(StringIO(content_input))) - - -def save_json_to_file(json_data, filename="permissions_config.json"): - with open(filename, "w") as json_file: - json.dump(json_data, json_file, indent=4) - - -def generate_csv_with_ordered_100000_rows(file_name=None): - """ - Generate a CSV where: - - 100 sets of (NEW → UPDATE → DELETE) are created. - - The 100 sets are shuffled but maintain the correct order within each set. - - The 300 shuffled sets are then randomly mixed into the 99,700 CREATE rows. - - The final dataset ensures all NEW rows come before UPDATE and DELETE in each set. - """ - total_rows = 100000 - special_row_count = 300 - unique_ids = [str(uuid.uuid4()) for _ in range(special_row_count // 3)] - special_data = [] - - # Generate first 300 rows as structured NEW → UPDATE → DELETE sets - for i in range(special_row_count // 3): # 100 sets - new_row = create_row( - unique_id=unique_ids[i], - fore_name="PHYLIS", - dose_amount="0.3", - action_flag="NEW", - header="NHS_NUMBER", - ) - update_row = create_row( - unique_id=unique_ids[i], - fore_name="PHYLIS", - dose_amount="0.4", - action_flag="UPDATE", - header="NHS_NUMBER", - ) - delete_row = create_row( - unique_id=unique_ids[i], - fore_name="PHYLIS", - dose_amount="0.1", - action_flag="DELETE", - header="NHS_NUMBER", - ) - - special_data.append((new_row, update_row, delete_row)) # Keep them as ordered tuples - - # Shuffle the sets (ensuring NEW is always first in each set) - random.shuffle(special_data) - - # Flatten while maintaining NEW → UPDATE → DELETE order inside each set - ordered_special_data = [row for set_group in special_data for row in set_group] - - # Generate remaining 99,700 rows as CREATE operations - create_data = [ - create_row( - unique_id=str(uuid.uuid4()), - action_flag="NEW", - dose_amount="0.3", - fore_name="PHYLIS", - header="NHS_NUMBER", - ) - for _ in range(total_rows - special_row_count) - ] - - # Combine 300 shuffled sets with 99,700 CREATE rows - full_data = create_data + ordered_special_data - - # Shuffle the entire dataset while ensuring "NEW" always comes before "UPDATE" and "DELETE" in each set - random.shuffle(full_data) - - # Sort data so that within each unique ID, "NEW" appears before "UPDATE" and "DELETE" - full_data.sort( - key=lambda x: ( - x["UNIQUE_ID"], - x["ACTION_FLAG"] != "NEW", - x["ACTION_FLAG"] == "DELETE", - ) - ) - - # Convert to DataFrame and save as CSV - df = pd.DataFrame(full_data) - timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%f")[:-3] - file_name = f"RSV_Vaccinations_v5_YGM41_{timestamp}.csv" if not file_name else file_name - df.to_csv(file_name, index=False, sep="|", quoting=csv.QUOTE_MINIMAL) - return file_name - - -def verify_final_ack_file(file_key): - """Verify if the final ack file has 100,000 rows and HEADER_RESPONSE_CODE column has only 'OK' values.""" - response = s3_client.get_object(Bucket=ACK_BUCKET, Key=file_key) - df = pd.read_csv(io.BytesIO(response["Body"].read()), delimiter="|") - - row_count = len(df) - # Check if all HEADER_RESPONSE_CODE values are "OK" - all_ok = df[HEADER_RESPONSE_CODE_COLUMN].nunique() == 1 and df[HEADER_RESPONSE_CODE_COLUMN].iloc[0] == "OK" - if row_count != 100000 or not all_ok: - raise AssertionError( - f"Final Ack file '{file_key}' failed validation. " - f"Row count: {row_count}" - f"Unique HEADER_RESPONSE_CODE values: {df[HEADER_RESPONSE_CODE_COLUMN].iloc[0]}" - f"All values OK: {all_ok}" - ) - return True - - -def delete_filename_from_audit_table(filename) -> bool: - # 1. Query the GSI to get all items with the given filename - try: - response = audit_table.query( - IndexName="filename_index", - KeyConditionExpression=Key("filename").eq(filename), - ) - items = response.get("Items", []) - - # 2. Delete each item by primary key (message_id) - for item in items: - audit_table.delete_item(Key={"message_id": item["message_id"]}) - return True - except Exception as e: - logger.error(f"Error deleting from audit table: {e}") - return False - - -def delete_filename_from_events_table(identifier) -> bool: - # 1. Query the GSI to get all items with the given filename - try: - identifier_pk = f"{RAVS_URI}#{identifier}" - response = events_table.query( - IndexName="IdentifierGSI", - KeyConditionExpression=Key("IdentifierPK").eq(identifier_pk), - ) - items = response.get("Items", []) - - # 2. Delete each item by primary key (PK) - for item in items: - events_table.delete_item(Key={"PK": item["PK"]}) - return True - except Exception as e: - logger.warning(f"Error deleting from events table: {e}") - return False - - -def poll_s3_file_pattern(prefix, search_pattern): - """Poll the ACK_BUCKET for an ack file that contains the input_file_name as a substring.""" - - response = s3_client.list_objects_v2(Bucket=ACK_BUCKET, Prefix=prefix) - if "Contents" in response: - for obj in response["Contents"]: - key = obj["Key"] - if search_pattern in key: - return key - return None - - -def aws_cleanup(key, identifier, ack_keys): - if key: - archive_file = f"archive/{key}" - if not delete_file_from_s3(SOURCE_BUCKET, archive_file): - logger.warning(f"S3 delete fail {SOURCE_BUCKET}: {archive_file}") - delete_filename_from_audit_table(key) - delete_filename_from_events_table(identifier) - for ack_key in ack_keys.values(): - if ack_key: - if not delete_file_from_s3(ACK_BUCKET, ack_key): - logger.warning(f"s3 delete fail {ACK_BUCKET}: {ack_key}") - - -def purge_sqs_queues() -> bool: - try: - # only purge if ENVIRONMENT=pr-* to avoid purging shared queues - if environment.startswith("pr-"): - sqs_client.purge_queue(QueueUrl=batch_fifo_queue_url) - sqs_client.purge_queue(QueueUrl=ack_metadata_queue_url) - return True - except sqs_client.exceptions.PurgeQueueInProgress: - logger.error("SQS purge already in progress. Try again later.") - except Exception as e: - logger.error(f"SQS Purge error: {e}") - return False - - -def create_row(unique_id, dose_amount, action_flag: str, header, inject_cp1252=None): - """Helper function to create a single row with the specified UNIQUE_ID and ACTION_FLAG.""" - - name = "James" if not inject_cp1252 else b"Jam\xe9s" - return { - header: "9732928395", - "PERSON_FORENAME": "PHYLIS", - "PERSON_SURNAME": name, - "PERSON_DOB": "20080217", - "PERSON_GENDER_CODE": "0", - "PERSON_POSTCODE": "WD25 0DZ", - "DATE_AND_TIME": datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S"), - "SITE_CODE": "RVVKC", - "SITE_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - "UNIQUE_ID": unique_id, - "UNIQUE_ID_URI": RAVS_URI, - "ACTION_FLAG": action_flag, - "PERFORMING_PROFESSIONAL_FORENAME": "PHYLIS", - "PERFORMING_PROFESSIONAL_SURNAME": name, - "RECORDED_DATE": datetime.now(timezone.utc).strftime("%Y%m%d"), - "PRIMARY_SOURCE": "TRUE", - "VACCINATION_PROCEDURE_CODE": "956951000000104", - "VACCINATION_PROCEDURE_TERM": "RSV vaccination in pregnancy (procedure)", - "DOSE_SEQUENCE": "1", - "VACCINE_PRODUCT_CODE": "42223111000001107", - "VACCINE_PRODUCT_TERM": "Quadrivalent influenza vaccine (Sanofi Pasteur)", - "VACCINE_MANUFACTURER": "Sanofi Pasteur", - "BATCH_NUMBER": "BN92478105653", - "EXPIRY_DATE": "20240915", - "SITE_OF_VACCINATION_CODE": "368209003", - "SITE_OF_VACCINATION_TERM": "Right arm", - "ROUTE_OF_VACCINATION_CODE": "1210999013", - "ROUTE_OF_VACCINATION_TERM": "Intradermal use", - "DOSE_AMOUNT": dose_amount, - "DOSE_UNIT_CODE": "2622896019", - "DOSE_UNIT_TERM": "Inhalation - unit of product usage", - "INDICATION_CODE": "1037351000000105", - "LOCATION_CODE": "RJC02", - "LOCATION_CODE_TYPE_URI": "https://fhir.nhs.uk/Id/ods-organization-code", - } diff --git a/tests/e2e_batch/vax_suppliers.py b/tests/e2e_batch/vax_suppliers.py deleted file mode 100644 index d205c603fb..0000000000 --- a/tests/e2e_batch/vax_suppliers.py +++ /dev/null @@ -1,138 +0,0 @@ -# json to represent the classes below -suppliers = { - "DPSFULL": { - "DPSFULL": { - "3IN1": "CRUDS", - "COVID": "CRUDS", - "FLU": "CRUDS", - "HPV": "CRUDS", - "MENACWY": "CRUDS", - "MMR": "CRUDS", - "RSV": "CRUDS", - } - }, - "DPSREDUCED": { - "DPSREDUCED": { - "3IN1": "CRUDS", - "COVID": "CRUDS", - "FLU": "CRUDS", - "HPV": "CRUDS", - "MENACWY": "CRUDS", - "MMR": "CRUDS", - "RSV": "CRUDS", - } - }, - "MAVIS": { - "V0V8L": { - "FLU": "CRUDS", - "HPV": "CUD", - } - }, - "SONAR": {"8HK48": {"FLU": "CD"}}, - "EVA": {"8HA94": {"COVID": "CUD"}}, - "RAVS": { - "X26": {"MMR": "CRUDS", "RSV": "CRUDS"}, - "X8E5B": {"MMR": "CRUDS", "RSV": "CRUDS"}, - }, - "EMIS": { - "YGM41": { - "3IN1": "CRUDS", - "COVID": "CRUDS", - "HPV": "CRUDS", - "MENACWY": "CRUDS", - "MMR": "CRUDS", - "RSV": "CRUDS", - }, - "YGJ": { - "3IN1": "CRUDS", - "COVID": "CRUDS", - "HPV": "CRUDS", - "MENACWY": "CRUDS", - "MMR": "CRUDS", - "RSV": "CRUDS", - }, - }, - "TPP": { - "YGA": { - "3IN1": "CRUDS", - "HPV": "CRUDS", - "MENACWY": "CRUDS", - "MMR": "CRUDS", - "RSV": "CRUDS", - } - }, - "MEDICUS": { - "YGMYW": { - "3IN1": "CRUDS", - "HPV": "CRUDS", - "MENACWY": "CRUDS", - "MMR": "CRUDS", - "RSV": "CRUDS", - } - }, -} - - -class OdsVax: - def __init__(self, ods_code: str, vax: str): - self.ods_code = ods_code - self.vax = vax - - -class TestPair: - """ - "ods_vax": TestPair.E8HA94_COVID_CUD, - "ods_vax": TestPair.DPSFULL_COVID_CRUDS, - "ods_vax": TestPair.V0V8L_FLU_CRUDS, - "ods_vax": TestPair.V0V8L_HPV_CUD, - "ods_vax": TestPair.X26_MMR_CRUDS, - "ods_vax": TestPair.YGA_MENACWY_CRUDS, - """ - - X26_MMR_CRUDS = OdsVax("X26", "MMR") - # X26_RSV_CRUDS = OdsVax("X26", "RSV") - # X8E5B_MMR_CRUDS = OdsVax("X8E5B", "MMR") - # X8E5B_RSV_CRUDS = OdsVax("X8E5B", "RSV") - # YGM41_3IN1_CRUDS = OdsVax("YGM41", "3IN1") - # YGM41_COVID_CRUDS = OdsVax("YGM41", "COVID") - # YGM41_HPV_CRUDS = OdsVax("YGM41", "HPV") - # YGM41_MENACWY_CRUDS = OdsVax("YGM41", "MENACWY") - # YGM41_MMR_CRUDS = OdsVax("YGM41", "MMR") - # YGM41_RSV_CRUDS = OdsVax("YGM41", "RSV") - # YGJ_3IN1_CRUDS = OdsVax("YGJ", "3IN1") - # YGJ_COVID_CRUDS = OdsVax("YGJ", "COVID") - # YGJ_HPV_CRUDS = OdsVax("YGJ", "HPV") - # YGJ_MENACWY_CRUDS = OdsVax("YGJ", "MENACWY") - # YGJ_MMR_CRUDS = OdsVax("YGJ", "MMR") - # YGJ_RSV_CRUDS = OdsVax("YGJ", "RSV") - # DPSFULL_3IN1_CRUDS = OdsVax("DPSFULL", "3IN1") - DPSFULL_COVID_CRUDS = OdsVax("DPSFULL", "COVID") - # DPSFULL_FLU_CRUDS = OdsVax("DPSFULL", "FLU") - # DPSFULL_HPV_CRUDS = OdsVax("DPSFULL", "HPV") - # DPSFULL_MENACWY_CRUDS = OdsVax("DPSFULL", "MENACWY") - # DPSFULL_MMR_CRUDS = OdsVax("DPSFULL", "MMR") - # DPSFULL_RSV_CRUDS = OdsVax("DPSFULL", "RSV") - # DPSREDUCED_3IN1_CRUDS = OdsVax("DPSREDUCED", "3IN1") - # DPSREDUCED_COVID_CRUDS = OdsVax("DPSREDUCED", "COVID") - # DPSREDUCED_FLU_CRUDS = OdsVax("DPSREDUCED", "FLU") - # DPSREDUCED_HPV_CRUDS = OdsVax("DPSREDUCED", "HPV") - # DPSREDUCED_MENACWY_CRUDS = OdsVax("DPSREDUCED", "MENACWY") - # DPSREDUCED_MMR_CRUDS = OdsVax("DPSREDUCED", "MMR") - # DPSREDUCED_RSV_CRUDS = OdsVax("DPSREDUCED", "RSV") - V0V8L_HPV_CUD = OdsVax("V0V8L", "HPV") - V0V8L_FLU_CRUDS = OdsVax("V0V8L", "FLU") - # V0V8L_HPV_CRUDS = OdsVax("V0V8L", "HPV") - # V0V8L_MENACWY_CRUDS = OdsVax("V0V8L", "MENACWY") - # V0V8L_MMR_CRUDS = OdsVax("V0V8L", "MMR") - # YGA_3IN1_CRUDS = OdsVax("YGA", "3IN1") - # YGA_HPV_CRUDS = OdsVax("YGA", "HPV") - YGA_MENACWY_CRUDS = OdsVax("YGA", "MENACWY") - # YGA_MMR_CRUDS = OdsVax("YGA", "MMR") - # YGA_RSV_CRUDS = OdsVax("YGA", "RSV") - # YGMYW_3IN1_CRUDS = OdsVax("YGMYW", "3IN1") - # YGMYW_HPV_CRUDS = OdsVax("YGMYW", "HPV") - # YGMYW_MENACWY_CRUDS = OdsVax("YGMYW", "MENACWY") - # YGMYW_MMR_CRUDS = OdsVax("YGMYW", "MMR") - # YGMYW_RSV_CRUDS = OdsVax("YGMYW", "RSV") - # E8HK48_FLU_CD = OdsVax("8HK48", "FLU") - E8HA94_COVID_CUD = OdsVax("8HA94", "COVID")