From 3384ef279281d7f72096f6ded99157ecf3ea8920 Mon Sep 17 00:00:00 2001 From: Hans Keeler Date: Wed, 8 Nov 2023 09:55:45 -0500 Subject: [PATCH] feat: Replace `main.py` with Typer-based CLI app (#63) ### Replace `main.py` to [Typer](https://typer.tiangolo.com/) CLI app The CLI setup in `main.py` is pretty primitive, and is really geared towards _us_, developers on this project. I would like to create a utility that outside technical users can use to test their files against our validation logic. To get there without having to reinvent many wheels, we need an actual CLI framework. That's where [Typer](https://typer.tiangolo.com/) comes in. Typer is a wrapper around [Click](https://click.palletsprojects.com/en/8.1.x/), written by the same dev as FastAPI, so it follows many of the same patterns as we're used to there, such as leaning into Python's `typing` and [Pydantic](https://docs.pydantic.dev/latest/) for example. ### Return validation results as Pandas `DataFrame` instead of vanilla `dict` I also wanted to support multiple output formats; at least json, csv, and default to a human-readable table. And while we _could_ do that with a `dict`, it's much easier to work with `DataFrame`s as they're already tabular in nature and have a ton of transform functionality built-in. So, I have also refactored the `validate` function to return a Panda's `DataFrame` instead of a vanilla `dict`. This made it much easier to output validation data in multiple formats. Along with this, I did a fair bit of refactoring of that surrounding code, breaking up the big monolith function into smaller testable functions. ### `cfpb-val` script To simply using the CLI, I've added a shortcut script so you don't have to call the full path to the Python script. In it's current form, you can now call just... cfpb-val --help ...instead of... python regtech_data_validator/cli.py --help #### Better script name? I'm not in love with `cfpb-val`. I started off with `cfpb-comply`, but I didn't love that either. I'm looking for something that's short, but also very clear that it's CFPB-related, and having to do with this project. `cfpb-rdv`? ### Lots of `README.md` love I've reworked the `README.md` quite a bit, focusing on users coming to this repo for the first time, and trying to get the CLI to work. This includes pushing the Poetry-based install up to the top and simplifying it, slimming and pushing down development-related setup, and adding details about how to use the CLI itself. See: https://github.com/cfpb/regtech-data-validator/blob/add-typer-cli/README.md ### Refactor `lei` arg to more generic `context` In attempt to future-proof this a bit, I've added a multi-value `context` arg to the CLI, which allows you to pass arbitrary `=` pairs, `lei` being the only key that's current used for anything. ### Other tidbits I found along the way - Removed deprecated `python.formatting.provider` VS Code configuration - Removes no longer used `Makefile` - Fixes several validations that had incorrect IDs, severities, or were in the wrong phase --------- Co-authored-by: lchen-2101 <73617864+lchen-2101@users.noreply.github.com> --- .devcontainer/devcontainer.json | 1 - .github/workflows/linters.yml | 1 - Makefile | 8 - README.md | 1371 +++---------------- poetry.lock | 338 ++--- pyproject.toml | 17 +- regtech_data_validator/check_functions.py | 1 - regtech_data_validator/cli.py | 155 +++ regtech_data_validator/create_schemas.py | 177 ++- regtech_data_validator/main.py | 50 - regtech_data_validator/phase_validations.py | 18 +- tests/test_cli.py | 230 ++++ tests/test_sample_data.py | 34 +- tests/test_schema_functions.py | 105 +- 14 files changed, 999 insertions(+), 1507 deletions(-) delete mode 100644 Makefile create mode 100644 regtech_data_validator/cli.py delete mode 100644 regtech_data_validator/main.py create mode 100644 tests/test_cli.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9656043b..1dc2c1ad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -40,7 +40,6 @@ ], "editor.tabSize": 4, "editor.formatOnSave": true, - "python.formatting.provider": "none", "python.envFile": "${workspaceFolder}/.env", "editor.codeActionsOnSave": { "source.organizeImports": true diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index db76efdf..1c672781 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -10,7 +10,6 @@ jobs: - uses: psf/black@stable with: options: "--check --diff --verbose" - version: "~= 22.0" ruff: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile deleted file mode 100644 index 99afd87e..00000000 --- a/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -lint: - ruff check src; \ - exit 0; - -lint_and_fix: - ruff check src --fix; \ - black src; \ - exit 0; \ No newline at end of file diff --git a/README.md b/README.md index becd0ac5..18edda07 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RegTech Data Validator -Current overall coverage: [![Coverage badge](https://github.com/cfpb/regtech-data-validator/raw/python-coverage-comment-action-data/badge.svg)](https://github.com/cfpb/regtech-data-validator/tree/python-coverage-comment-action-data) +[![Coverage badge](https://github.com/cfpb/regtech-data-validator/raw/python-coverage-comment-action-data/badge.svg)](https://github.com/cfpb/regtech-data-validator/tree/python-coverage-comment-action-data) Python-based tool for parsing and validating CFPB's RegTech-related data submissions. It uses the [Pandera](https://pandera.readthedocs.io/en/stable/) data testing @@ -13,46 +13,137 @@ We are currently focused on implementing the SBL (Small Business Lending) data submission. For details on this dataset and its validations, please see: [Filing instructions guide for small business lending data collected in 2024](https://www.consumerfinance.gov/data-research/small-business-lending/filing-instructions-guide/2024-guide/) -## Pre-requisites +## Setup -The following software packages are pre-requisites to installing this software. +The following setup must be completed prior to running the CLI utilities or doing any +development on the project. + +### Prerequisites + +The following software packages are prerequisites to installing this software. - [Python](https://www.python.org/downloads/) version 3.11 or greater. - [Poetry](https://python-poetry.org/docs/#installation) for Python package management. -## Dependencies +### Install -All packages and libraries used in this repository can be found in [`pyproject.toml`](https://github.com/cfpb/regtech-data-validator/blob/main/pyproject.toml) +1. Checkout this project -## Contributing + ```sh + git clone https://github.com/cfpb/regtech-data-validator.git + cd regtech-data-validator + ``` -[CFPB](https://www.consumerfinance.gov/) is developing the -`RegTech Data Validator` in the open to maximize transparency and encourage -third party contributions. If you want to contribute, please read and abide by -the terms of the [License](./LICENSE) for this project. Pull Requests are always -welcome. +1. Install Python packages via Poetry -## Contact Us + ```sh + poetry install + ``` -If you have an inquiry or suggestion for the validator or any SBL related code -please reach out to us at +1. Activate Poetry's virtual environment -## Development + ```sh + poetry shell + ``` -There are few files in `src/validator` that will be of interest. +**Note:** All Python packages used in project can be found in +[`pyproject.toml`](https://github.com/cfpb/regtech-data-validator/blob/main/pyproject.toml) -- `checks.py` defines custom Pandera Check class called `SBLCheck`. -- `global_data.py` defines functions to parse NAICS and GEOIDs. -- `phase_validations.py` defines phase 1 and phase 2 Pandera schema/checks used - for validating the SBLAR data. -- `check_functions.py` contains a collection of functions to be run against the - data that are a bit too complex to be implemented directly within the schema - as Lambda functions. -- Lastly, the file `main.py` pulls everything together and illustrates how the - schema can catch the various validation errors present in our mock, invalid - dataset and different LEI values. +## Usage -Unit tests that can be located under [`src/tests`](https://github.com/cfpb/regtech-data-validator/tree/main/src/tests). +This project includes the `cfpb-val` CLI utility for validating CFPB's RegTech-related +data collection file formats. It currently supports the small business lending (SBL) data +collected for 2024, but may support more formats in the future. This tool is intended for +testing purposes, allowing a quick way to check the validity of a file without having +to submit it through the full CFPB-hosted filing systems. + +### Validating data + +``` +$ cfpb-val validate --help + + Usage: cfpb-val validate [OPTIONS] PATH + +╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────────╮ +│ * path FILE [default: None] [required] │ +╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────╮ +│ --context = [example: lei=12345678901234567890] │ +│ --output [csv|json|pandas|table] [default: table] │ +│ --help Show this message and exit. │ +╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ +``` + +#### Examples + +1. Validate file with no findings + + $ cfpb-val validate tests/data/sbl-validations-pass.csv + + **Note:** No output is returned if the file contains no validations errors or warnings. + +1. Validate file with findings, passing in LEI as context + + $ cfpb-val validate tests/data/sbl-validations-fail.csv --context lei=000TESTFIUIDDONOTUSE + + ╭────────────┬───────────┬──────────────────┬────────────────────────────────────────────────────┬─────────────────────┬───────────────┬──────────────────────────────────────╮ + │ finding_no │ record_no │ field_name │ field_value │ validation_severity │ validation_id │ validation_name │ + ├────────────┼───────────┼──────────────────┼────────────────────────────────────────────────────┼─────────────────────┼───────────────┼──────────────────────────────────────┤ + │ 1 │ 4 │ uid │ 000TESTFIUIDDONOTUSEXBXVID13XTC1 │ error │ E3000 │ uid.duplicates_in_dataset │ + │ 2 │ 5 │ uid │ 000TESTFIUIDDONOTUSEXBXVID13XTC1 │ error │ E3000 │ uid.duplicates_in_dataset │ + │ 3 │ 0 │ uid │ │ error │ E0001 │ uid.invalid_text_length │ + │ 4 │ 1 │ uid │ BXUIDXVID11XTC2 │ error │ E0001 │ uid.invalid_text_length │ + │ 5 │ 2 │ uid │ BXUIDXVID11XTC31234567890123456789012345678901 │ error │ E0001 │ uid.invalid_text_length │ + │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ + │ 115 │ 278 │ po_4_race_baa_ff │ 12345678901234567890123456789012345678901234567890 │ error │ E1000 │ po_4_race_baa_ff.invalid_text_length │ + │ 116 │ 290 │ po_4_race_pi_ff │ 12345678901234567890123456789012345678901234567890 │ error │ E1020 │ po_4_race_pi_ff.invalid_text_length │ + │ 117 │ 302 │ po_4_gender_flag │ 9001 │ error │ E1040 │ po_4_gender_flag.invalid_enum_value │ + │ 118 │ 306 │ po_4_gender_ff │ 12345678901234567890123456789012345678901234567890 │ error │ E1060 │ po_4_gender_ff.invalid_text_length │ + ╰────────────┴───────────┴──────────────────┴────────────────────────────────────────────────────┴─────────────────────┴───────────────┴──────────────────────────────────────╯ + +1. Validate file with findings with JSON output + + $ cfpb-val validate tests/data/sbl-validations-fail.csv --output json + + [ + { + "validation": { + "id": "E0001", + "name": "uid.invalid_text_length", + "description": "'Unique identifier' must be at least 21 characters in length and at most 45 characters in length.", + "severity": "error" + }, + "records": [ + { + "record_no": 0, + "fields": [ + { + "name": "uid", + "value": "" + } + ] + }, + { + "record_no": 1, + "fields": [ + { + "name": "uid", + "value": "BXUIDXVID11XTC2" + } + ] + }, + { + "record_no": 2, + "fields": [ + { + "name": "uid", + "value": "BXUIDXVID11XTC31234567890123456789012345678901" + } + ] + } + ] + }, + ... ## Test Data @@ -60,1178 +151,156 @@ This repo includes 2 test datasets, one with all valid data, and one where each line represents a different failed validation, or different permutation of the same failed validation. -- [`sbl-validations-pass.csv`](src/tests/data/sbl-validations-pass.csv) -- [`sbl-validations-fail.csv`](src/tests/data/sbl-validations-fail.csv) +- [`sbl-validations-pass.csv`](tests/data/sbl-validations-pass.csv) +- [`sbl-validations-fail.csv`](tests/data/sbl-validations-fail.csv) We use these test files in for automated test, but can also be passed in via the -CLI for manual testing. +`cfpb-val` CLI utility for manual testing. -## Development Process and Standard -Development Process -Below are the steps the development team follows to fix issues, develop new features, -etc. +## Development -1. Work in a branch -2. Create a PR to merge into main -3. The PR is automatically built, tested, and linted using github actions with PyTest, - Black, Ruff, and Coverage. -4. Manual review is performed in addition to ensuring the above automatic scans - are positive -5. The PR is deployed to development servers to be checked -6. The PR is merged only by a separate member in the dev team +### Best practices -Development standard practice +#### `Check` functions - Check functions should focus on reuse. - - Most of the validations share logic with other validations. + - Most of the validations share logic with other validations. - Avoid using lambdas for Check functions. - - They do not promote reuse. - - They are harder to debug. - - They are harder to test. + - They do not promote reuse. + - They are harder to debug. + - They are harder to test. - Check function signatures should reflect the functionality. - Check functions should have corresponding unit tests. - - [Unit Test](./src/tests/test_check_functions.py) + - [Unit Test](tests/test_check_functions.py) - Check definitions' name should be set to validation ID. - - Example: "denial_reasons. enum_value_conflict" - ![Validation ID](images/validation_id.png) + - Example: "denial_reasons. enum_value_conflict" + ![Validation ID](images/validation_id.png) - Check new added lines are formatted correctly. -## Installing Dependencies - -Run `poetry install` to install dependencies defined in `pyproject.toml` - -
- See Terminal Output - -```sh - -$ cd ~/Projects/regtech-data-validator - -$ poetry install - -Installing dependencies from lock file - -Package operations: 25 installs, 0 updates, 0 removals - - • Installing six (1.16.0) - • Installing iniconfig (2.0.0) - • Installing mypy-extensions (1.0.0) - • Installing numpy (1.25.2) - • Installing packaging (23.1) - • Installing pluggy (1.3.0) - • Installing python-dateutil (2.8.2) - • Installing pytz (2023.3.post1) - • Installing typing-extensions (4.7.1) - • Installing tzdata (2023.3) - • Installing click (8.1.7): Pending... - • Installing coverage (7.3.1): Pending... - • Installing coverage (7.3.1): Pending... - • Installing click (8.1.7): Downloading... 0% - • Installing coverage (7.3.1): Pending... - • Installing coverage (7.3.1): Downloading... 0% - • Installing coverage (7.3.1): Downloading... 0% - • Installing click (8.1.7): Downloading... 20% - • Installing coverage (7.3.1): Downloading... 0% - • Installing coverage (7.3.1): Downloading... 10% - • Installing coverage (7.3.1): Downloading... 10% - • Installing click (8.1.7): Downloading... 62% - • Installing coverage (7.3.1): Downloading... 10% - • Installing coverage (7.3.1): Downloading... 30% - • Installing coverage (7.3.1): Downloading... 30% - • Installing click (8.1.7): Downloading... 100% - • Installing coverage (7.3.1): Downloading... 30% - • Installing coverage (7.3.1): Downloading... 30% - • Installing click (8.1.7): Installing... - • Installing coverage (7.3.1): Downloading... 30% - • Installing coverage (7.3.1): Downloading... 30% - • Installing click (8.1.7) - • Installing coverage (7.3.1): Downloading... 30% - • Installing coverage (7.3.1): Downloading... 61% - • Installing coverage (7.3.1): Downloading... 91% - • Installing coverage (7.3.1): Downloading... 100% - • Installing coverage (7.3.1): Installing... - • Installing coverage (7.3.1) - • Installing multimethod (1.9.1) - • Installing pandas (2.1.0) - • Installing pathspec (0.11.2) - • Installing platformdirs (3.10.0) - • Installing pydantic (1.10.12) - • Installing pytest (7.4.0) - • Installing typeguard (4.1.3) - • Installing typing-inspect (0.9.0) - • Installing wrapt (1.15.0) - • Installing black (23.3.0) - • Installing pandera (0.16.1) - • Installing pytest-cov (4.1.0) - • Installing ruff (0.0.259) - -``` - -
- -## Running Validator - -`main.py` allows user to test csv file with and without LEI number - -```sh -# Running validator using LEI and CSV file -main.py - -# Running validator using just CSV file -main.py -``` - -When all validations passed, it prints out : - -```sh -[{'response': 'No validations errors or warnings'}] -``` - -When validation(s) failed, it prints out JSON data containing failed validation(s) - -```sh -# Example of JSON response containing failed validation -[ - { - 'validation': - { - 'id': 'E3000', - 'name': 'uid.duplicates_in_dataset', - 'description': "Any 'unique identifier' may not be used in more than one - record within a small business lending application register.", - 'fields': ['uid'], - 'severity': 'error' - }, - 'records': [ - { - 'number': 5, - 'field_values': {'uid': '000TESTFIUIDDONOTUSEXBXVID13XTC1'} - }, - { - 'number': 6, - 'field_values': {'uid': '000TESTFIUIDDONOTUSEXBXVID13XTC1'} - } - ] - } -] - -``` -To run `main.py` in terminal, you can use these commands. +## Testing -```sh -# Test validating the "good" file -# If passing lei value, pass lei as first arg and csv_path as second arg -$ poetry run python src/validator/main.py 000TESTFIUIDDONOTUSE src/tests/data/sbl-validations-pass.csv -# else just pass the csv_path as arg -$ poetry run python src/validator/main.py src/tests/data/sbl-validations-pass.csv - -# Test validating the "bad" file -$ poetry run python src/validator/main.py 000TESTFIUIDDONOTUSE src/tests/data/sbl-validations-fail.csv -# or -$ poetry run python src/validator/main.py src/tests/data/sbl-validations-fail.csv -``` +This project uses [pytest](https://docs.pytest.org/) for automated testing. -
- Example of Validator with Valid Data +### Running tests -```sh -$ poetry run python src/validator/main.py src/tests/data/sbl-validations-pass.csv -[{'response': 'No validations errors or warnings'}] ``` - - -
- -
- Example of Validator with Incorrect Data - -```sh - -$ poetry run python src/validator/main.py src/tests/data/sbl-validations-fail.csv -[{'records': [{'field_values': {'uid': '000TESTFIUIDDONOTUSEXBXVID13XTC1'}, - 'number': 5}, - {'field_values': {'uid': '000TESTFIUIDDONOTUSEXBXVID13XTC1'}, - 'number': 6}], - 'validation': {'description': "Any 'unique identifier' may not be used in " - 'more than one record within a small business ' - 'lending application register.', - 'fields': ['uid'], - 'id': 'E3000', - 'name': 'uid.duplicates_in_dataset', - 'severity': 'error'}}, - {'records': [{'field_values': {'uid': ''}, 'number': 1}, - {'field_values': {'uid': 'BXUIDXVID11XTC2'}, 'number': 2}, - {'field_values': {'uid': 'BXUIDXVID11XTC31234567890123456789012345678901'}, - 'number': 3}], - 'validation': {'description': "'Unique identifier' must be at least 21 " - 'characters in length and at most 45 ' - 'characters in length.', - 'fields': ['uid'], - 'id': 'E0001', - 'name': 'uid.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'uid': ''}, 'number': 1}, - {'field_values': {'uid': 'BXUIDXVID12XTC1abcdef'}, 'number': 4}], - 'validation': {'description': "'Unique identifier' may contain any " - 'combination of numbers and/or uppercase ' - 'letters (i.e., 0-9 and A-Z), and must not ' - 'contain any other characters.', - 'fields': ['uid'], - 'id': 'E0002', - 'name': 'uid.invalid_text_pattern', - 'severity': 'error'}}, - {'records': [{'field_values': {'app_date': ''}, 'number': 8}, - {'field_values': {'app_date': '12012024'}, 'number': 9}], - 'validation': {'description': "'Application date' must be a real calendar " - 'date using YYYYMMDD format.', - 'fields': ['app_date'], - 'id': 'E0020', - 'name': 'app_date.invalid_date_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'app_method': ''}, 'number': 10}, - {'field_values': {'app_method': '9001'}, 'number': 11}], - 'validation': {'description': "'Application method' must equal 1, 2, 3, or " - '4.', - 'fields': ['app_method'], - 'id': 'E0040', - 'name': 'app_method.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'app_recipient': ''}, 'number': 12}, - {'field_values': {'app_recipient': '9001'}, 'number': 13}], - 'validation': {'description': "'Application recipient' must equal 1 or 2", - 'fields': ['app_recipient'], - 'id': 'E0060', - 'name': 'app_recipient.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'ct_credit_product': ''}, 'number': 14}, - {'field_values': {'ct_credit_product': '9001'}, 'number': 15}], - 'validation': {'description': "'Credit product' must equal 1, 2, 3, 4, 5, 6, " - '7, 8, 977, or 988.', - 'fields': ['ct_credit_product'], - 'id': 'E0080', - 'name': 'ct_credit_product.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'ct_credit_product_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 16}], - 'validation': {'description': "'Free-form text field for other credit " - "products' must not exceed 300 characters in " - 'length.', - 'fields': ['ct_credit_product_ff'], - 'id': 'E0100', - 'name': 'ct_credit_product_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'ct_guarantee': '9001'}, 'number': 19}, - {'field_values': {'ct_guarantee': ''}, 'number': 20}], - 'validation': {'description': "Each value in 'type of guarantee' (separated " - 'by semicolons) must equal 1, 2, 3, 4, 5, 6, ' - '7, 8, 9, 10, 11, 977, or 999.', - 'fields': ['ct_guarantee'], - 'id': 'E0120', - 'name': 'ct_guarantee.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'ct_guarantee_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 24}], - 'validation': {'description': "'Free-form text field for other guarantee' " - 'must not exceed 300 characters in length', - 'fields': ['ct_guarantee_ff'], - 'id': 'E0140', - 'name': 'ct_guarantee_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'ct_loan_term_flag': ''}, 'number': 29}, - {'field_values': {'ct_loan_term_flag': '9001'}, 'number': 30}, - {'field_values': {'ct_loan_term_flag': '1'}, 'number': 33}], - 'validation': {'description': "Each value in 'Loan term: NA/NP flag' " - '(separated by semicolons) must equal 900, ' - '988, or 999.', - 'fields': ['ct_loan_term_flag'], - 'id': 'E0160', - 'name': 'ct_loan_term_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'ct_loan_term': 'must be blank'}, - 'number': 36}], - 'validation': {'description': "When present, 'loan term' must be a whole " - 'number.', - 'fields': ['ct_loan_term'], - 'id': 'E0180', - 'name': 'ct_loan_term.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'credit_purpose': '1;2;9001'}, 'number': 39}, - {'field_values': {'credit_purpose': ''}, 'number': 40}], - 'validation': {'description': "Each value in 'credit purpose' (separated by " - 'semicolons) must equal 1, 2, 3, 4, 5, 6, 7, ' - '8, 9, 10, 11, 977, 988, or 999.', - 'fields': ['credit_purpose'], - 'id': 'E0200', - 'name': 'credit_purpose.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'credit_purpose_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 45}], - 'validation': {'description': "'Free-form text field for other credit " - "purpose' must not exceed 300 characters in " - 'length', - 'fields': ['credit_purpose_ff'], - 'id': 'E0220', - 'name': 'credit_purpose_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'amount_applied_for_flag': ''}, 'number': 50}, - {'field_values': {'amount_applied_for_flag': '9001'}, - 'number': 51}], - 'validation': {'description': "'Amount applied For: NA/NP flag' must equal " - '900, 988, or 999.', - 'fields': ['amount_applied_for_flag'], - 'id': 'E0240', - 'name': 'amount_applied_for_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'amount_applied_for': 'nonNumericValue'}, - 'number': 52}, - {'field_values': {'amount_applied_for': 'must be blank'}, - 'number': 55}], - 'validation': {'description': "When present, 'amount applied for' must be a " - 'numeric value.', - 'fields': ['amount_applied_for'], - 'id': 'E0260', - 'name': 'amount_applied_for.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'amount_approved': 'nonNumericValue'}, - 'number': 56}], - 'validation': {'description': "When present, 'amount approved or originated' " - 'must be a numeric value.', - 'fields': ['amount_approved'], - 'id': 'E0280', - 'name': 'amount_approved.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'action_taken': ''}, 'number': 63}, - {'field_values': {'action_taken': '9001'}, 'number': 64}], - 'validation': {'description': "'Action taken' must equal 1, 2, 3, 4, or 5.", - 'fields': ['action_taken'], - 'id': 'E0300', - 'name': 'action_taken.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'action_taken_date': '12312024'}, - 'number': 65}], - 'validation': {'description': "'Action taken date' must be a real calendar " - 'date using YYYYMMDD format.', - 'fields': ['action_taken_date'], - 'id': 'E0320', - 'name': 'action_taken_date.invalid_date_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'denial_reasons': '9001'}, 'number': 70}, - {'field_values': {'denial_reasons': ''}, 'number': 71}, - {'field_values': {'denial_reasons': '999;1; 2'}, 'number': 78}], - 'validation': {'description': "Each value in 'denial reason(s)' (separated " - 'by semicolons)must equal 1, 2, 3, 4, 5, 6, 7, ' - '8, 9, 977, or 999.', - 'fields': ['denial_reasons'], - 'id': 'E0001', - 'name': 'denial_reasons.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'denial_reasons_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 80}], - 'validation': {'description': "'Free-form text field for other denial " - "reason(s)'must not exceed 300 characters in " - 'length.', - 'fields': ['denial_reasons_ff'], - 'id': 'E0360', - 'name': 'denial_reasons_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_interest_rate_type': ''}, - 'number': 85}, - {'field_values': {'pricing_interest_rate_type': '9001'}, - 'number': 86}, - {'field_values': {'pricing_interest_rate_type': '900'}, - 'number': 87}, - {'field_values': {'pricing_interest_rate_type': '900'}, - 'number': 94}, - {'field_values': {'pricing_interest_rate_type': '900'}, - 'number': 101}], - 'validation': {'description': "Each value in 'Interest rate type' (separated " - 'by semicolons) Must equal 1, 2, 3, 4, 5, 6, ' - 'or 999', - 'fields': ['pricing_interest_rate_type'], - 'id': 'E0380', - 'name': 'pricing_interest_rate_type.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_init_rate_period': 'nonNumericValue'}, - 'number': 118}], - 'validation': {'description': ("When present, 'initial rate period' must be " - 'a whole number.',), - 'fields': ['pricing_init_rate_period'], - 'id': 'E0400', - 'name': 'pricing_init_rate_period.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_fixed_rate': 'nonNumericValue'}, - 'number': 127}], - 'validation': {'description': "When present, 'fixed rate: interest rate' " - 'must be a numeric value.', - 'fields': ['pricing_fixed_rate'], - 'id': 'E0420', - 'name': 'pricing_fixed_rate.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_adj_index_name': ''}, 'number': 145}, - {'field_values': {'pricing_adj_index_name': '9001'}, - 'number': 146}], - 'validation': {'description': "'Adjustable rate transaction: index name' " - 'must equal 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ' - '977, or 999.', - 'fields': ['pricing_adj_index_name'], - 'id': 'E0460', - 'name': 'pricing_adj_index_name.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_adj_index_name_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 154}], - 'validation': {'description': "'Adjustable rate transaction: index name: " - "other' must not exceed 300 characters in " - 'length.', - 'fields': ['pricing_adj_index_name_ff'], - 'id': 'E0480', - 'name': 'pricing_adj_index_name_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_adj_index_value': 'nonNumericValue'}, - 'number': 157}], - 'validation': {'description': "When present, 'adjustable rate transaction: " - "index value' must be a numeric value.", - 'fields': ['pricing_adj_index_value'], - 'id': 'E0500', - 'name': 'pricing_adj_index_value.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_origination_charges': 'nonNumericValue'}, - 'number': 165}], - 'validation': {'description': ("When present, 'total origination charges' " - 'must be a numeric', - 'value.'), - 'fields': ['pricing_origination_charges'], - 'id': 'E0520', - 'name': 'pricing_origination_charges.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_broker_fees': 'nonNumericValue'}, - 'number': 166}], - 'validation': {'description': ("When present, 'amount of total broker fees' " - 'must be a', - 'numeric value.'), - 'fields': ['pricing_broker_fees'], - 'id': 'E0540', - 'name': 'pricing_broker_fees.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_initial_charges': 'nonNumericValue'}, - 'number': 167}], - 'validation': {'description': "When present, 'initial annual charges' must " - 'be anumeric value.', - 'fields': ['pricing_initial_charges'], - 'id': 'E0560', - 'name': 'pricing_initial_charges.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_mca_addcost_flag': ''}, 'number': 168}, - {'field_values': {'pricing_mca_addcost_flag': '99009001'}, - 'number': 169}], - 'validation': {'description': "'MCA/sales-based: additional cost for " - 'merchant cash advances or other sales-based ' - "financing: NA flag' must equal 900 or 999.", - 'fields': ['pricing_mca_addcost_flag'], - 'id': 'E0580', - 'name': 'pricing_mca_addcost_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_mca_addcost': 'nonNumericValue'}, - 'number': 171}, - {'field_values': {'pricing_mca_addcost': 'must be blank'}, - 'number': 172}], - 'validation': {'description': "When present, 'MCA/sales-based: additional " - 'cost for merchant cash advances or other ' - "sales-based financing' must be a numeric " - 'value', - 'fields': ['pricing_mca_addcost'], - 'id': 'E0600', - 'name': 'pricing_mca_addcost.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_prepenalty_allowed': ''}, - 'number': 174}, - {'field_values': {'pricing_prepenalty_allowed': '9001'}, - 'number': 175}], - 'validation': {'description': "'Prepayment penalty could be imposed' must " - 'equal 1, 2, or 999.', - 'fields': ['pricing_prepenalty_allowed'], - 'id': 'E0620', - 'name': 'pricing_prepenalty_allowed.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'pricing_prepenalty_exists': ''}, - 'number': 176}, - {'field_values': {'pricing_prepenalty_exists': '9001'}, - 'number': 177}], - 'validation': {'description': "'Prepayment penalty exists' must equal 1, 2, " - 'or 999.', - 'fields': ['pricing_prepenalty_exists'], - 'id': 'E0640', - 'name': 'pricing_prepenalty_exists.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'census_tract_adr_type': ''}, 'number': 178}, - {'field_values': {'census_tract_adr_type': '9001'}, - 'number': 179}], - 'validation': {'description': "'Census tract: type of address' must equal 1, " - '2, 3, or 988.', - 'fields': ['census_tract_adr_type'], - 'id': 'E0640', - 'name': 'census_tract_adr_type.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'census_tract_number': '1234567890'}, - 'number': 181}, - {'field_values': {'census_tract_number': 'must be blank'}, - 'number': 182}], - 'validation': {'description': "When present, 'census tract: tract number' " - 'must be a GEOID with exactly 11 digits.', - 'fields': ['census_tract_number'], - 'id': 'E0680', - 'name': 'census_tract_number.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'gross_annual_revenue_flag': ''}, - 'number': 187}, - {'field_values': {'gross_annual_revenue_flag': '99009001'}, - 'number': 188}], - 'validation': {'description': "'Gross annual revenue: NP flag' must equal " - '900 or 988.', - 'fields': ['gross_annual_revenue_flag'], - 'id': 'E0700', - 'name': 'gross_annual_revenue_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'gross_annual_revenue': 'nonNumericValue'}, - 'number': 189}, - {'field_values': {'gross_annual_revenue': 'must be blank'}, - 'number': 190}], - 'validation': {'description': "When present, 'gross annual revenue' must be " - 'a numeric value.', - 'fields': ['gross_annual_revenue'], - 'id': 'E0720', - 'name': 'gross_annual_revenue.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'naics_code_flag': ''}, 'number': 192}, - {'field_values': {'naics_code_flag': '9001'}, 'number': 193}], - 'validation': {'description': "'North American Industry Classification " - "System (NAICS) code: NP flag' must equal 900 " - 'or 988.', - 'fields': ['naics_code_flag'], - 'id': 'E0720', - 'name': 'naics_code_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'naics_code': 'notDigits'}, 'number': 196}], - 'validation': {'description': "'North American Industry Classification " - "System (NAICS) code' may only contain numeric " - 'characters.', - 'fields': ['naics_code'], - 'id': 'E0761', - 'name': 'naics_code.invalid_naics_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'number_of_workers': ''}, 'number': 199}, - {'field_values': {'number_of_workers': '9001'}, 'number': 200}], - 'validation': {'description': "'Number of workers' must equal 1, 2, 3, 4, 5, " - '6, 7, 8, 9, or 988.', - 'fields': ['number_of_workers'], - 'id': 'E0780', - 'name': 'number_of_workers.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'time_in_business_type': ''}, 'number': 201}, - {'field_values': {'time_in_business_type': '9001'}, - 'number': 202}], - 'validation': {'description': "'Time in business: type of response' must " - 'equal 1, 2, 3, or 988.', - 'fields': ['time_in_business_type'], - 'id': 'E0800', - 'name': 'time_in_business_type.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'time_in_business': 'must be blank'}, - 'number': 205}], - 'validation': {'description': "When present, 'time in business' must be a " - 'whole number.', - 'fields': ['time_in_business'], - 'id': 'E0820', - 'name': 'time_in_business.invalid_numeric_format', - 'severity': 'error'}}, - {'records': [{'field_values': {'business_ownership_status': '1;2; 9001'}, - 'number': 207}, - {'field_values': {'business_ownership_status': ''}, - 'number': 208}], - 'validation': {'description': "Each value in 'business ownership status' " - '(separated by semicolons) must equal 1, 2, 3, ' - '955, 966, or 988.', - 'fields': ['business_ownership_status'], - 'id': 'E0840', - 'name': 'business_ownership_status.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'num_principal_owners_flag': ''}, - 'number': 211}, - {'field_values': {'num_principal_owners_flag': '9001'}, - 'number': 212}], - 'validation': {'description': "'Number of principal owners: NP flag' must " - 'equal 900 or 988.', - 'fields': ['num_principal_owners_flag'], - 'id': 'E0860', - 'name': 'num_principal_owners_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'num_principal_owners': '9001'}, 'number': 213}, - {'field_values': {'num_principal_owners': 'must be blank'}, - 'number': 214}], - 'validation': {'description': "When present, 'number of principal owners' " - 'must equal 0, 1, 2, 3, or 4.', - 'fields': ['num_principal_owners'], - 'id': 'E0880', - 'name': 'num_principal_owners.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_ethnicity': '9001;1'}, 'number': 216}], - 'validation': {'description': "When present, each value in 'ethnicity of " - "principal owner 1' (separated by semicolons) " - 'must equal 1, 11, 12, 13, 14, 2, 966, 977, or ' - '988.', - 'fields': ['po_1_ethnicity'], - 'id': 'E0900', - 'name': 'po_1_ethnicity.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_ethnicity_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 228}], - 'validation': {'description': "'Ethnicity of principal owner 1: free-form " - "text field for other Hispanic or Latino' must " - 'not exceed 300 characters in length.', - 'fields': ['po_1_ethnicity_ff'], - 'id': 'E0920', - 'name': 'po_1_ethnicity_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_race': '9001;1'}, 'number': 240}], - 'validation': {'description': "When present, each value in 'race of " - "principal owner 1' (separated by semicolons) " - 'must equal 1, 2, 21, 22, 23, 24, 25, 26, 27, ' - '3, 31, 32, 33, 34, 35, 36, 37, 4, 41, 42, 43, ' - '44, 5, 966, 971, 972, 973, 974, or 988.', - 'fields': ['po_1_race'], - 'id': 'E0940', - 'name': 'po_1_race.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_race_anai_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 252}], - 'validation': {'description': "'Race of principal owner 1: free-form text " - 'field for American Indian or Alaska Native ' - "Enrolled or Principal Tribe' must not exceed " - '300 characters in length.', - 'fields': ['po_1_race_anai_ff'], - 'id': 'E0960', - 'name': 'po_1_race_anai_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_race_asian_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 264}], - 'validation': {'description': "'Race of principal owner 1: free-form text " - "field for other Asian' must not exceed 300 " - 'characters in length.', - 'fields': ['po_1_race_asian_ff'], - 'id': 'E0980', - 'name': 'po_1_race_asian_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_race_baa_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 276}], - 'validation': {'description': "'Race of principal owner 1: free-form text " - "field for other Black or African American' " - 'must not exceed 300 characters in length.', - 'fields': ['po_1_race_baa_ff'], - 'id': 'E1000', - 'name': 'po_1_race_baa_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_race_pi_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 288}], - 'validation': {'description': "'Race of principal owner 1: free-form text " - "field for other Pacific Islander race' must " - 'not exceed 300 characters in length.', - 'fields': ['po_1_race_pi_ff'], - 'id': 'E1020', - 'name': 'po_1_race_pi_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_gender_flag': '9001'}, 'number': 300}], - 'validation': {'description': "When present, 'sex/gender of principal owner " - "1: NP flag' must equal 1, 966, or 988.", - 'fields': ['po_1_gender_flag'], - 'id': 'E1040', - 'name': 'po_1_gender_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_1_gender_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 304}], - 'validation': {'description': "'Sex/gender of principal owner 1: free-form " - "text field for self-identified sex/gender' " - 'must not exceed 300 characters in length.', - 'fields': ['po_1_gender_ff'], - 'id': 'E1060', - 'name': 'po_1_gender_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_ethnicity': '9001;1'}, 'number': 217}], - 'validation': {'description': "When present, each value in 'ethnicity of " - "principal owner 2' (separated by semicolons) " - 'must equal 1, 11, 12, 13, 14, 2, 966, 977, or ' - '988.', - 'fields': ['po_2_ethnicity'], - 'id': 'E0900', - 'name': 'po_2_ethnicity.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_ethnicity_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 229}], - 'validation': {'description': "'Ethnicity of principal owner 2: free-form " - "text field for other Hispanic or Latino' must " - 'not exceed 300 characters in length.', - 'fields': ['po_2_ethnicity_ff'], - 'id': 'E0920', - 'name': 'po_2_ethnicity_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_race': '9001;1'}, 'number': 241}], - 'validation': {'description': "When present, each value in 'race of " - "principal owner 2' (separated by semicolons) " - 'must equal 1, 2, 21, 22, 23, 24, 25, 26, 27, ' - '3, 31, 32, 33, 34, 35, 36, 37, 4, 41, 42, 43, ' - '44, 5, 966, 971, 972, 973, 974, or 988.', - 'fields': ['po_2_race'], - 'id': 'E0940', - 'name': 'po_2_race.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_race_anai_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 253}], - 'validation': {'description': "'Race of principal owner 2: free-form text " - 'field for American Indian or Alaska Native ' - "Enrolled or Principal Tribe' must not exceed " - '300 characters in length.', - 'fields': ['po_2_race_anai_ff'], - 'id': 'E0960', - 'name': 'po_2_race_anai_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_race_asian_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 265}], - 'validation': {'description': "'Race of principal owner 2: free-form text " - "field for other Asian' must not exceed 300 " - 'characters in length.', - 'fields': ['po_2_race_asian_ff'], - 'id': 'E0980', - 'name': 'po_2_race_asian_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_race_baa_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 277}], - 'validation': {'description': "'Race of principal owner 2: free-form text " - "field for other Black or African American' " - 'must not exceed 300 characters in length.', - 'fields': ['po_2_race_baa_ff'], - 'id': 'E1000', - 'name': 'po_2_race_baa_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_race_pi_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 289}], - 'validation': {'description': "'Race of principal owner 2: free-form text " - "field for other Pacific Islander race' must " - 'not exceed 300 characters in length.', - 'fields': ['po_2_race_pi_ff'], - 'id': 'E1020', - 'name': 'po_2_race_pi_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_gender_flag': '9001'}, 'number': 301}], - 'validation': {'description': "When present, 'sex/gender of principal owner " - "2: NP flag' must equal 1, 966, or 988.", - 'fields': ['po_2_gender_flag'], - 'id': 'E1040', - 'name': 'po_2_gender_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_2_gender_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 305}], - 'validation': {'description': "'Sex/gender of principal owner 2: free-form " - "text field for self-identified sex/gender' " - 'must not exceed 300 characters in length.', - 'fields': ['po_2_gender_ff'], - 'id': 'E1060', - 'name': 'po_2_gender_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_ethnicity': '9001;1'}, 'number': 218}], - 'validation': {'description': "When present, each value in 'ethnicity of " - "principal owner 3' (separated by semicolons) " - 'must equal 1, 11, 12, 13, 14, 2, 966, 977, or ' - '988.', - 'fields': ['po_3_ethnicity'], - 'id': 'E0900', - 'name': 'po_3_ethnicity.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_ethnicity_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 230}], - 'validation': {'description': "'Ethnicity of principal owner 3: free-form " - "text field for other Hispanic or Latino' must " - 'not exceed 300 characters in length.', - 'fields': ['po_3_ethnicity_ff'], - 'id': 'E0920', - 'name': 'po_3_ethnicity_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_race': '9001;1'}, 'number': 242}], - 'validation': {'description': "When present, each value in 'race of " - "principal owner 3' (separated by semicolons) " - 'must equal 1, 2, 21, 22, 23, 24, 25, 26, 27, ' - '3, 31, 32, 33, 34, 35, 36, 37, 4, 41, 42, 43, ' - '44, 5, 966, 971, 972, 973, 974, or 988.', - 'fields': ['po_3_race'], - 'id': 'E0940', - 'name': 'po_3_race.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_race_anai_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 254}], - 'validation': {'description': "'Race of principal owner 3: free-form text " - 'field for American Indian or Alaska Native ' - "Enrolled or Principal Tribe' must not exceed " - '300 characters in length.', - 'fields': ['po_3_race_anai_ff'], - 'id': 'E0960', - 'name': 'po_3_race_anai_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_race_asian_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 266}], - 'validation': {'description': "'Race of principal owner 3: free-form text " - "field for other Asian' must not exceed 300 " - 'characters in length.', - 'fields': ['po_3_race_asian_ff'], - 'id': 'E0980', - 'name': 'po_3_race_asian_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_race_baa_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 278}], - 'validation': {'description': "'Race of principal owner 3: free-form text " - "field for other Black or African American' " - 'must not exceed 300 characters in length.', - 'fields': ['po_3_race_baa_ff'], - 'id': 'E1000', - 'name': 'po_3_race_baa_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_race_pi_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 290}], - 'validation': {'description': "'Race of principal owner 3: free-form text " - "field for other Pacific Islander race' must " - 'not exceed 300 characters in length.', - 'fields': ['po_3_race_pi_ff'], - 'id': 'E1020', - 'name': 'po_3_race_pi_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_gender_flag': '9001'}, 'number': 302}], - 'validation': {'description': "When present, 'sex/gender of principal owner " - "3: NP flag' must equal 1, 966, or 988.", - 'fields': ['po_3_gender_flag'], - 'id': 'E1040', - 'name': 'po_3_gender_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_3_gender_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 306}], - 'validation': {'description': "'Sex/gender of principal owner 3: free-form " - "text field for self-identified sex/gender' " - 'must not exceed 300 characters in length.', - 'fields': ['po_3_gender_ff'], - 'id': 'E1060', - 'name': 'po_3_gender_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_ethnicity': '9001;1'}, 'number': 219}], - 'validation': {'description': "When present, each value in 'ethnicity of " - "principal owner 4' (separated by semicolons) " - 'must equal 1, 11, 12, 13, 14, 2, 966, 977, or ' - '988.', - 'fields': ['po_4_ethnicity'], - 'id': 'E0900', - 'name': 'po_4_ethnicity.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_ethnicity_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 231}], - 'validation': {'description': "'Ethnicity of principal owner 4: free-form " - "text field for other Hispanic or Latino' must " - 'not exceed 300 characters in length.', - 'fields': ['po_4_ethnicity_ff'], - 'id': 'E0920', - 'name': 'po_4_ethnicity_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_race': '9001;1'}, 'number': 243}], - 'validation': {'description': "When present, each value in 'race of " - "principal owner 4' (separated by semicolons) " - 'must equal 1, 2, 21, 22, 23, 24, 25, 26, 27, ' - '3, 31, 32, 33, 34, 35, 36, 37, 4, 41, 42, 43, ' - '44, 5, 966, 971, 972, 973, 974, or 988.', - 'fields': ['po_4_race'], - 'id': 'E0940', - 'name': 'po_4_race.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_race_anai_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 255}], - 'validation': {'description': "'Race of principal owner 4: free-form text " - 'field for American Indian or Alaska Native ' - "Enrolled or Principal Tribe' must not exceed " - '300 characters in length.', - 'fields': ['po_4_race_anai_ff'], - 'id': 'E0960', - 'name': 'po_4_race_anai_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_race_asian_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 267}], - 'validation': {'description': "'Race of principal owner 4: free-form text " - "field for other Asian' must not exceed 300 " - 'characters in length.', - 'fields': ['po_4_race_asian_ff'], - 'id': 'E0980', - 'name': 'po_4_race_asian_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_race_baa_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 279}], - 'validation': {'description': "'Race of principal owner 4: free-form text " - "field for other Black or African American' " - 'must not exceed 300 characters in length.', - 'fields': ['po_4_race_baa_ff'], - 'id': 'E1000', - 'name': 'po_4_race_baa_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_race_pi_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 291}], - 'validation': {'description': "'Race of principal owner 4: free-form text " - "field for other Pacific Islander race' must " - 'not exceed 300 characters in length.', - 'fields': ['po_4_race_pi_ff'], - 'id': 'E1020', - 'name': 'po_4_race_pi_ff.invalid_text_length', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_gender_flag': '9001'}, 'number': 303}], - 'validation': {'description': "When present, 'sex/gender of principal owner " - "4: NP flag' must equal 1, 966, or 988.", - 'fields': ['po_4_gender_flag'], - 'id': 'E1040', - 'name': 'po_4_gender_flag.invalid_enum_value', - 'severity': 'error'}}, - {'records': [{'field_values': {'po_4_gender_ff': '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890XXX'}, - 'number': 307}], - 'validation': {'description': "'Sex/gender of principal owner 4: free-form " - "text field for self-identified sex/gender' " - 'must not exceed 300 characters in length.', - 'fields': ['po_4_gender_ff'], - 'id': 'E1060', - 'name': 'po_4_gender_ff.invalid_text_length', - 'severity': 'error'}}] - -``` - -
- -## Running Test - -This repository is using `pytest`. If using VS Code, tests can be completed within -a Dev Container. If using local terminal or console, you can use this command -`poetry run pytest` in the root directory - -
- See Example of Pytest Output - -```sh - -$ poetry run pytest -================================================================================== -test session starts ================================================================================== -platform darwin -- Python 3.11.5, pytest-7.4.0, pluggy-1.3.0 -- /Library/Caches/pypoetry/virtualenvs/regtech-data-validator-uJQWmvcM-py3.11/bin/python +$ pytest +======================================================= test session starts ======================================================= +platform darwin -- Python 3.11.2, pytest-7.4.0, pluggy-1.3.0 -- /Users/keelerh/Library/Caches/pypoetry/virtualenvs/regtech-data-validator-Sa0Sf38s-py3.11/bin/python cachedir: .pytest_cache -rootdir: /Projects/regtech-data-validator +rootdir: /Users/keelerh/Projects/regtech-data-validator configfile: pyproject.toml -testpaths: src/tests +testpaths: tests plugins: cov-4.1.0, typeguard-4.1.3 -collected 117 items - -src/tests/test_check_functions.py::TestInvalidDateFormat::test_with_valid_date <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 0%] -src/tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_date <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 1%] -src/tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_day <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 2%] -src/tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_month <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 3%] -src/tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_year <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 4%] -src/tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_format <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 5%] -src/tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_type <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 5%] -src/tests/test_check_functions.py::TestDuplicatesInField::test_with_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 6%] -src/tests/test_check_functions.py::TestDuplicatesInField::test_with_no_duplicates <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 7%] -src/tests/test_check_functions.py::TestDuplicatesInField::test_with_duplicates <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 8%] -src/tests/test_check_functions.py::TestInvalidNumberOfValues::test_with_in_range <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 9%] -src/tests/test_check_functions.py::TestInvalidNumberOfValues::test_with_lower_range_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 10%] -src/tests/test_check_functions.py::TestInvalidNumberOfValues::test_with_invalid_lower_range_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 11%] -src/tests/test_check_functions.py::TestInvalidNumberOfValues::test_with_upper_range_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 11%] -src/tests/test_check_functions.py::TestInvalidNumberOfValues::test_with_invalid_upper_range_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 12%] -src/tests/test_check_functions.py::TestInvalidNumberOfValues::test_valid_with_no_upper_bound <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 13%] -src/tests/test_check_functions.py::TestInvalidNumberOfValues::test_invalid_with_no_upper_bound <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 14%] -src/tests/test_check_functions.py::TestMultiValueFieldRestriction::test_with_invalid_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 15%] -src/tests/test_check_functions.py::TestMultiValueFieldRestriction::test_with_valid_length <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 16%] -src/tests/test_check_functions.py::TestMultiValueFieldRestriction::test_with_valid_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 17%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_inside_maxlength <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 17%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_on_maxlength <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 18%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_with_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 19%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_invalid_length_with_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 20%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_invalid_length_with_blank_and_ignored_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 21%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_valid_length_with_blank_and_ignored_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 22%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_outside_maxlength <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 23%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_valid_length_with_non_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 23%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_invalid_length_with_non_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 24%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_valid_length_with_ignored_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 25%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_invalid_length_with_ignored_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 26%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_valid_length_with_blank_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 27%] -src/tests/test_check_functions.py::TestMultiInvalidNumberOfValues::test_invalid_length_with_blank_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 28%] -src/tests/test_check_functions.py::TestInvalidEnumValue::test_with_valid_enum_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 29%] -src/tests/test_check_functions.py::TestInvalidEnumValue::test_with_is_valid_enums <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 29%] -src/tests/test_check_functions.py::TestInvalidEnumValue::test_with_valid_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 30%] -src/tests/test_check_functions.py::TestInvalidEnumValue::test_with_invalid_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 31%] -src/tests/test_check_functions.py::TestIsNumber::test_number_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 32%] -src/tests/test_check_functions.py::TestIsNumber::test_non_number_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 33%] -src/tests/test_check_functions.py::TestIsNumber::test_decimal_numeric_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 34%] -src/tests/test_check_functions.py::TestIsNumber::test_alphanumeric_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 35%] -src/tests/test_check_functions.py::TestIsNumber::test_negative_numeric_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 35%] -src/tests/test_check_functions.py::TestIsNumber::test_negative_decimal_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 36%] -src/tests/test_check_functions.py::TestIsNumber::test_valid_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 37%] -src/tests/test_check_functions.py::TestIsNumber::test_invalid_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 38%] -src/tests/test_check_functions.py::TestConditionalFieldConflict::test_conditional_field_conflict_correct <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 39%] -src/tests/test_check_functions.py::TestConditionalFieldConflict::test_conditional_field_conflict_incorrect <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 40%] -src/tests/test_check_functions.py::TestEnumValueConflict::test_enum_value_confict_correct <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 41%] -src/tests/test_check_functions.py::TestEnumValueConflict::test_enum_value_confict_incorrect <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 41%] -src/tests/test_check_functions.py::TestHasCorrectLength::test_with_accept_blank_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 42%] -src/tests/test_check_functions.py::TestHasCorrectLength::test_with_invalid_blank_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 43%] -src/tests/test_check_functions.py::TestHasCorrectLength::test_with_correct_length <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 44%] -src/tests/test_check_functions.py::TestHasCorrectLength::test_with_incorrect_length <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 45%] -src/tests/test_check_functions.py::TestIsValidCode::test_with_valid_code <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 46%] -src/tests/test_check_functions.py::TestIsValidCode::test_with_invalid_code <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 47%] -src/tests/test_check_functions.py::TestIsValidCode::test_with_accepted_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 47%] -src/tests/test_check_functions.py::TestIsValidCode::test_with_invalid_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 48%] -src/tests/test_check_functions.py::TestIsGreaterThan::test_with_greater_min_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 49%] -src/tests/test_check_functions.py::TestIsGreaterThan::test_with_smaller_min_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 50%] -src/tests/test_check_functions.py::TestIsGreaterThan::test_with_equal_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 51%] -src/tests/test_check_functions.py::TestIsGreaterThan::test_with_valid_blank_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 52%] -src/tests/test_check_functions.py::TestIsGreaterThan::test_with_invalid_blank_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 52%] -src/tests/test_check_functions.py::TestIsGreaterThanOrEqualTo::test_with_greater_min_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 53%] -src/tests/test_check_functions.py::TestIsGreaterThanOrEqualTo::test_with_smaller_min_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 54%] -src/tests/test_check_functions.py::TestIsGreaterThanOrEqualTo::test_with_equal_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 55%] -src/tests/test_check_functions.py::TestIsGreaterThanOrEqualTo::test_with_valid_blank_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 56%] -src/tests/test_check_functions.py::TestIsGreaterThanOrEqualTo::test_with_invalid_blank_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 57%] -src/tests/test_check_functions.py::TestIsLessThan::test_with_greater_max_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 58%] -src/tests/test_check_functions.py::TestIsLessThan::test_with_less_max_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 58%] -src/tests/test_check_functions.py::TestIsLessThan::test_with_equal_max_value <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 59%] -src/tests/test_check_functions.py::TestIsLessThan::test_with_valid_blank_space <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 60%] -src/tests/test_check_functions.py::TestIsLessThan::test_with_invalid_blank_space <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 61%] -src/tests/test_check_functions.py::TestHasValidFormat::test_with_valid_data_alphanumeric <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 62%] -src/tests/test_check_functions.py::TestHasValidFormat::test_with_invalid_data_alphanumeric <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 63%] -src/tests/test_check_functions.py::TestHasValidFormat::test_with_accepting_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 64%] -src/tests/test_check_functions.py::TestHasValidFormat::test_with_not_accepting_blank <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 64%] -src/tests/test_check_functions.py::TestHasValidFormat::test_with_valid_data_ip <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 65%] -src/tests/test_check_functions.py::TestHasValidFormat::test_with_invalid_data_ip <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 66%] -src/tests/test_check_functions.py::TestIsUniqueColumn::test_with_valid_series <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 67%] -src/tests/test_check_functions.py::TestIsUniqueColumn::test_with_multiple_valid_series <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 68%] -src/tests/test_check_functions.py::TestIsUniqueColumn::test_with_invalid_series <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 69%] -src/tests/test_check_functions.py::TestIsUniqueColumn::test_with_multiple_items_series <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 70%] -src/tests/test_check_functions.py::TestIsUniqueColumn::test_with_multiple_invalid_series <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 70%] -src/tests/test_check_functions.py::TestIsUniqueColumn::test_with_multiple_mix_series <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 71%] -src/tests/test_check_functions.py::TestIsUniqueColumn::test_with_blank_value_series <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 72%] -src/tests/test_check_functions.py::TestHasValidFieldsetPair::test_with_correct_is_not_equal_condition <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 73%] -src/tests/test_check_functions.py::TestHasValidFieldsetPair::test_with_correct_is_equal_condition <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 74%] -src/tests/test_check_functions.py::TestHasValidFieldsetPair::test_with_correct_is_equal_and_not_equal_conditions <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 75%] -src/tests/test_check_functions.py::TestHasValidFieldsetPair::test_with_value_not_in_condition_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 76%] -src/tests/test_check_functions.py::TestHasValidFieldsetPair::test_with_incorrect_is_not_equal_condition <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 76%] -src/tests/test_check_functions.py::TestHasValidFieldsetPair::test_with_incorrect_is_equal_condition <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 77%] -src/tests/test_check_functions.py::TestHasValidFieldsetPair::test_with_incorrect_is_equal_and_not_equal_conditions <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 78%] -src/tests/test_check_functions.py::TestIsValidId::test_with_correct_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 79%] -src/tests/test_check_functions.py::TestIsValidId::test_with_incorrect_values <- ../../../../workspaces/regtech-data-validator/src/tests/test_check_functions.py PASSED [ 80%] -src/tests/test_checks.py::TestSBLCheck::test_no_id_check <- ../../../../workspaces/regtech-data-validator/src/tests/test_checks.py PASSED [ 81%] -src/tests/test_checks.py::TestSBLCheck::test_no_name_check <- ../../../../workspaces/regtech-data-validator/src/tests/test_checks.py PASSED [ 82%] -src/tests/test_checks.py::TestSBLCheck::test_name_and_id_check <- ../../../../workspaces/regtech-data-validator/src/tests/test_checks.py PASSED [ 82%] -src/tests/test_global_data.py::TestGlobalData::test_valid_naics_codes <- ../../../../workspaces/regtech-data-validator/src/tests/test_global_data.py PASSED [ 83%] -src/tests/test_global_data.py::TestGlobalData::test_valid_geoids <- ../../../../workspaces/regtech-data-validator/src/tests/test_global_data.py PASSED [ 84%] -src/tests/test_global_data.py::TestGlobalData::test_invalid_naics_file <- ../../../../workspaces/regtech-data-validator/src/tests/test_global_data.py PASSED [ 85%] -src/tests/test_global_data.py::TestGlobalData::test_invalid_geoids_file <- ../../../../workspaces/regtech-data-validator/src/tests/test_global_data.py PASSED [ 86%] -src/tests/test_sample_data.py::TestValidatingSampleData::test_invalid_data_file PASSED [ 87%] -src/tests/test_sample_data.py::TestValidatingSampleData::test_run_validation_on_good_data_invalid_lei PASSED [ 88%] -src/tests/test_sample_data.py::TestValidatingSampleData::test_run_validation_on_good_data_valid_lei PASSED [ 88%] -src/tests/test_sample_data.py::TestValidatingSampleData::test_run_validation_on_bad_data_invalid_lei PASSED [ 89%] -src/tests/test_sample_data.py::TestValidatingSampleData::test_run_validation_on_bad_data_valid_lei PASSED [ 90%] -src/tests/test_schema_functions.py::TestValidate::test_with_valid_dataframe <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 91%] -src/tests/test_schema_functions.py::TestValidate::test_with_valid_lei <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 92%] -src/tests/test_schema_functions.py::TestValidate::test_with_invalid_dataframe <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 93%] -src/tests/test_schema_functions.py::TestValidate::test_with_multi_invalid_dataframe <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 94%] -src/tests/test_schema_functions.py::TestValidate::test_with_invalid_lei <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 94%] -src/tests/test_schema_functions.py::TestValidatePhases::test_with_valid_data <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 95%] -src/tests/test_schema_functions.py::TestValidatePhases::test_with_valid_lei <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 96%] -src/tests/test_schema_functions.py::TestValidatePhases::test_with_invalid_data <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 97%] -src/tests/test_schema_functions.py::TestValidatePhases::test_with_multi_invalid_data_with_phase1 <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 98%] -src/tests/test_schema_functions.py::TestValidatePhases::test_with_multi_invalid_data_with_phase2 <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [ 99%] -src/tests/test_schema_functions.py::TestValidatePhases::test_with_invalid_lei <- ../../../../workspaces/regtech-data-validator/src/tests/test_schema_functions.py PASSED [100%] - ----------- coverage: platform darwin, python 3.11.5-final-0 ---------- -Name Stmts Miss Branch BrPart Cover Missing --------------------------------------------------------------------------------- -src/tests/__init__.py 4 0 0 0 100% -src/tests/test_check_functions.py 418 0 0 0 100% -src/tests/test_checks.py 20 2 10 3 83% 9->exit, 16->exit, 24->exit, 25-26 -src/tests/test_global_data.py 19 0 4 0 100% -src/tests/test_sample_data.py 38 0 2 0 100% -src/tests/test_schema_functions.py 78 0 0 0 100% -src/validator/__init__.py 4 0 0 0 100% -src/validator/check_functions.py 184 14 78 0 91% 55-59, 111-121, 275-276, 297-298, 420-421 -src/validator/checks.py 14 0 4 0 100% -src/validator/create_schemas.py 55 1 18 2 96% 69, 74->49 -src/validator/global_data.py 18 0 4 0 100% -src/validator/main.py 25 25 8 0 0% 8-47 -src/validator/phase_validations.py 6 0 0 0 100% -src/validator/schema_template.py 6 0 0 0 100% --------------------------------------------------------------------------------- -TOTAL 889 42 128 5 94% +collected 112 items + +tests/test_check_functions.py::TestInvalidDateFormat::test_with_valid_date PASSED [ 0%] +tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_date PASSED [ 1%] +tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_day PASSED [ 2%] +tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_month PASSED [ 3%] +tests/test_check_functions.py::TestInvalidDateFormat::test_with_invalid_year PASSED [ 4%] +... +tests/test_schema_functions.py::TestValidatePhases::test_with_valid_lei PASSED [ 96%] +tests/test_schema_functions.py::TestValidatePhases::test_with_invalid_data PASSED [ 97%] +tests/test_schema_functions.py::TestValidatePhases::test_with_multi_invalid_data_with_phase1 PASSED [ 98%] +tests/test_schema_functions.py::TestValidatePhases::test_with_multi_invalid_data_with_phase2 PASSED [ 99%] +tests/test_schema_functions.py::TestValidatePhases::test_with_invalid_lei PASSED [100%] + + +---------- coverage: platform darwin, python 3.11.2-final-0 ---------- +Name Stmts Miss Branch BrPart Cover Missing +----------------------------------------------------------------------------------------- +regtech_data_validator/check_functions.py 184 14 78 0 91% 55-59, 111-121, 275-276, 297-298, 420-421 +regtech_data_validator/checks.py 15 0 2 0 100% +regtech_data_validator/cli.py 62 62 30 0 0% 1-126 +regtech_data_validator/create_schemas.py 61 3 18 3 92% 94, 99, 133 +regtech_data_validator/global_data.py 12 0 8 0 100% +regtech_data_validator/phase_validations.py 6 0 0 0 100% +regtech_data_validator/schema_template.py 6 0 0 0 100% +----------------------------------------------------------------------------------------- +TOTAL 346 79 136 3 75% + +3 empty files skipped. Coverage XML written to file coverage.xml +``` -================================================================================= 117 passed in 25.14s ================================================================================== -``` +### Test Coverage + +Test coverage details can be found in this project's +[`python-coverage-comment-action-data`](https://github.com/cfpb/regtech-data-validator/tree/python-coverage-comment-action-data) +branch. -
-## Running Linter +## Linting This repository utilizing `black` and `ruff` libraries to check and fix any -formatting issues +formatting issues. ```sh # Example of Ruff with an error -$ poetry run ruff src/ -src/tests/test_check_functions.py:205:26: E712 [*] Comparison to `False` should be `cond is False` +$ ruff . +tests/test_check_functions.py:205:26: E712 [*] Comparison to `False` should be `cond is False` Found 1 error. [*] 1 potentially fixable with the --fix option. # Example of black with reformatted line -$ poetry run black src/ -reformatted /Projects/regtech-data-validator/src/validator/main.py +$ black . +reformatted regtech_data_validator/cli.py All done! ✨ 🍰 ✨ 1 file reformatted, 13 files left unchanged. ``` -## (Optional) Using Dev Container and Visual Studio Code Development Setup +## (Optional) Visual Studio Code (VS Code) with Dev Containers setup -Requirements when using Visual Studio Code for Development: +This setup uses [VS Code's Dev Containers extension](https://code.visualstudio.com/docs/devcontainers/containers) +to create a standardized development environment, allowing for a quick setup, and a greater guarantee +that you'll have all the tools you need to start developing. It does rely heavily on +[Docker](https://docs.docker.com/), so some familiarity there is highly recommended. -- Visual Studio Code with Dev Containers extension -- Docker. +### Prerequisites -These instructions will not work if using an alternative editor such as Vim or - Emacs: +- [Docker](https://docs.docker.com/desktop/) +- [Visual Studio Code](https://code.visualstudio.com/Download) + - [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) -- Open this repository within VS Code and press `COMMAND + SHIFT + p` on your - keyboard. This will open the command bar at the top of your window. -- Enter `Dev Containers: Rebuild and Reopen in Container`. VS Code will open a - new window and you'll see a status message towards the bottom right of your - screen that the container is building and attaching. -- This will take a few minutes the first time because Docker needs to build the - container without a build cache. -- You may receive a notification that VS Code wants to perform a reload because - some extensions could not load. Sometimes this happens because extensions are - loaded in conflicting orders and dependencies are not satisfied. -- Unit tests can be run through VS Code Test. - ![VS Code Test](images/vscode_test.png) -- Validator can be executed by running `main.py` within a Dev Container. To run - `main.py`, you can run these commands in VSCode terminal. +### Setup instructions -```sh -# Test validating the "good" file -# If passing lei value, pass lei as first arg and csv_path as second arg -python src/validator/main.py 000TESTFIUIDDONOTUSE src/tests/data/sbl-validations-pass.csv -# else just pass the csv_path as arg -python src/validator/main.py src/tests/data/sbl-validations-pass.csv - -# Test validating the "bad" file -python src/validator/main.py 000TESTFIUIDDONOTUSE src/tests/data/sbl-validations-fail.csv -# or -python src/validator/main.py src/tests/data/sbl-validations-fail.csv -``` +1. Open this repository within VS Code and press `COMMAND + SHIFT + p` on your + keyboard. This will open the command bar at the top of your window. +1. Enter `Dev Containers: Rebuild and Reopen in Container`. VS Code will open a + new window and you'll see a status message towards the bottom right of your + screen that the container is building and attaching. +1. This will take a few minutes the first time because Docker needs to build the + container without a build cache. +1. You may receive a notification that VS Code wants to perform a reload because + some extensions could not load. Sometimes this happens because extensions are + loaded in conflicting orders and dependencies are not satisfied. -## Code Coverage -Complete coverage details can be found in [(`python-coverage-comment-action-data`)](https://github.com/cfpb/regtech-data-validator/tree/python-coverage-comment-action-data) +### Running unit tests + +Unit tests can be run through VS Code Test. + +![VS Code Test](images/vscode_test.png) + + +## Contributing + +[CFPB](https://www.consumerfinance.gov/) is developing the +`RegTech Data Validator` in the open to maximize transparency and encourage +third party contributions. If you want to contribute, please read and abide by +the terms of the [License](./LICENSE) for this project. Pull Requests are always +welcome. + +## Contact Us + +If you have an inquiry or suggestion for the validator or any SBL related code +please reach out to us at + ## Open source licensing info diff --git a/poetry.lock b/poetry.lock index 600691d0..5d1b552e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,36 +2,29 @@ [[package]] name = "black" -version = "23.3.0" +version = "23.10.1" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, + {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, + {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, + {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, + {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, + {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, + {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, + {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, + {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, + {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, + {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, ] [package.dependencies] @@ -74,63 +67,63 @@ files = [ [[package]] name = "coverage" -version = "7.3.1" +version = "7.3.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"}, - {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"}, - {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"}, - {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"}, - {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"}, - {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"}, - {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"}, - {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"}, - {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"}, - {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"}, - {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"}, - {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"}, - {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"}, - {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"}, - {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"}, - {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"}, - {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"}, - {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"}, - {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"}, - {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"}, - {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"}, - {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] [package.extras] @@ -160,13 +153,13 @@ files = [ [[package]] name = "multimethod" -version = "1.9.1" +version = "1.10" description = "Multiple argument dispatching." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "multimethod-1.9.1-py3-none-any.whl", hash = "sha256:52f8f1f2b9d5a4c7adfdcc114dbeeebe3245a4420801e8807e26522a79fb6bc2"}, - {file = "multimethod-1.9.1.tar.gz", hash = "sha256:1589bf52ca294667fd15527ea830127c763f5bfc38562e3642591ffd0fd9d56f"}, + {file = "multimethod-1.10-py3-none-any.whl", hash = "sha256:afd84da9c3d0445c84f827e4d63ad42d17c6d29b122427c6dee9032ac2d2a0d4"}, + {file = "multimethod-1.10.tar.gz", hash = "sha256:daa45af3fe257f73abb69673fd54ddeaf31df0eb7363ad6e1251b7c9b192d8c5"}, ] [[package]] @@ -230,13 +223,13 @@ et-xmlfile = "*" [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] @@ -345,13 +338,13 @@ files = [ [[package]] name = "platformdirs" -version = "3.10.0" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] @@ -375,47 +368,47 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "1.10.12" +version = "1.10.13" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, ] [package.dependencies] @@ -490,28 +483,28 @@ files = [ [[package]] name = "ruff" -version = "0.0.259" -description = "An extremely fast Python linter, written in Rust." +version = "0.1.4" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.259-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:f3938dc45e2a3f818e9cbd53007265c22246fbfded8837b2c563bf0ebde1a226"}, - {file = "ruff-0.0.259-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:22e1e35bf5f12072cd644d22afd9203641ccf258bc14ff91aa1c43dc14f6047d"}, - {file = "ruff-0.0.259-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2fb20e89e85d147c85caa807707a1488bccc1f3854dc3d53533e89b52a0c5ff"}, - {file = "ruff-0.0.259-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49e903bcda19f6bb0725a962c058eb5d61f40d84ef52ed53b61939b69402ab4e"}, - {file = "ruff-0.0.259-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71f0ef1985e9a6696fa97da8459917fa34bdaa2c16bd33bd5edead585b7d44f7"}, - {file = "ruff-0.0.259-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7cfef26619cba184d59aa7fa17b48af5891d51fc0b755a9bc533478a10d4d066"}, - {file = "ruff-0.0.259-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79b02fa17ec1fd8d306ae302cb47fb614b71e1f539997858243769bcbe78c6d9"}, - {file = "ruff-0.0.259-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:428507fb321b386dda70d66cd1a8aa0abf51d7c197983d83bb9e4fa5ee60300b"}, - {file = "ruff-0.0.259-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5fbaea9167f1852757f02133e5daacdb8c75b3431343205395da5b10499927a"}, - {file = "ruff-0.0.259-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:40ae87f2638484b7e8a7567b04a7af719f1c484c5bf132038b702bb32e1f6577"}, - {file = "ruff-0.0.259-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:29e2b77b7d5da6a7dd5cf9b738b511355c5734ece56f78e500d4b5bffd58c1a0"}, - {file = "ruff-0.0.259-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b3c1beacf6037e7f0781d4699d9a2dd4ba2462f475be5b1f45cf84c4ba3c69d"}, - {file = "ruff-0.0.259-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:daaea322e7e85f4c13d82be9536309e1c4b8b9851bb0cbc7eeb15d490fd46bf9"}, - {file = "ruff-0.0.259-py3-none-win32.whl", hash = "sha256:38704f151323aa5858370a2f792e122cc25e5d1aabe7d42ceeab83da18f0b456"}, - {file = "ruff-0.0.259-py3-none-win_amd64.whl", hash = "sha256:aa9449b898287e621942cc71b9327eceb8f0c357e4065fecefb707ef2d978df8"}, - {file = "ruff-0.0.259-py3-none-win_arm64.whl", hash = "sha256:e4f39e18702de69faaaee3969934b92d7467285627f99a5b6ecd55a7d9f5d086"}, - {file = "ruff-0.0.259.tar.gz", hash = "sha256:8b56496063ab3bfdf72339a5fbebb8bd46e5c5fee25ef11a9f03b208fa0562ec"}, + {file = "ruff-0.1.4-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:864958706b669cce31d629902175138ad8a069d99ca53514611521f532d91495"}, + {file = "ruff-0.1.4-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9fdd61883bb34317c788af87f4cd75dfee3a73f5ded714b77ba928e418d6e39e"}, + {file = "ruff-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4eaca8c9cc39aa7f0f0d7b8fe24ecb51232d1bb620fc4441a61161be4a17539"}, + {file = "ruff-0.1.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a9a1301dc43cbf633fb603242bccd0aaa34834750a14a4c1817e2e5c8d60de17"}, + {file = "ruff-0.1.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e8db8ab6f100f02e28b3d713270c857d370b8d61871d5c7d1702ae411df683"}, + {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:80fea754eaae06335784b8ea053d6eb8e9aac75359ebddd6fee0858e87c8d510"}, + {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bc02a480d4bfffd163a723698da15d1a9aec2fced4c06f2a753f87f4ce6969c"}, + {file = "ruff-0.1.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862811b403063765b03e716dac0fda8fdbe78b675cd947ed5873506448acea4"}, + {file = "ruff-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58826efb8b3efbb59bb306f4b19640b7e366967a31c049d49311d9eb3a4c60cb"}, + {file = "ruff-0.1.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdfd453fc91d9d86d6aaa33b1bafa69d114cf7421057868f0b79104079d3e66e"}, + {file = "ruff-0.1.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e8791482d508bd0b36c76481ad3117987301b86072158bdb69d796503e1c84a8"}, + {file = "ruff-0.1.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01206e361021426e3c1b7fba06ddcb20dbc5037d64f6841e5f2b21084dc51800"}, + {file = "ruff-0.1.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:645591a613a42cb7e5c2b667cbefd3877b21e0252b59272ba7212c3d35a5819f"}, + {file = "ruff-0.1.4-py3-none-win32.whl", hash = "sha256:99908ca2b3b85bffe7e1414275d004917d1e0dfc99d497ccd2ecd19ad115fd0d"}, + {file = "ruff-0.1.4-py3-none-win_amd64.whl", hash = "sha256:1dfd6bf8f6ad0a4ac99333f437e0ec168989adc5d837ecd38ddb2cc4a2e3db8a"}, + {file = "ruff-0.1.4-py3-none-win_arm64.whl", hash = "sha256:d98ae9ebf56444e18a3e3652b3383204748f73e247dea6caaf8b52d37e6b32da"}, + {file = "ruff-0.1.4.tar.gz", hash = "sha256:21520ecca4cc555162068d87c747b8f95e1e95f8ecfcbbe59e8dd00710586315"}, ] [[package]] @@ -525,15 +518,29 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + [[package]] name = "typeguard" -version = "4.1.3" +version = "4.1.5" description = "Run-time type checker for Python" optional = false python-versions = ">=3.8" files = [ - {file = "typeguard-4.1.3-py3-none-any.whl", hash = "sha256:5b7453b1e3b35fcfe2d62fa4ec500d05e6f2f2eb46f4126ae964677fcc384fff"}, - {file = "typeguard-4.1.3.tar.gz", hash = "sha256:7d4264cd631ac1157c5bb5ec992281b4f1e2ba7a35db91bc15f442235e244803"}, + {file = "typeguard-4.1.5-py3-none-any.whl", hash = "sha256:8923e55f8873caec136c892c3bed1f676eae7be57cdb94819281b3d3bc9c0953"}, + {file = "typeguard-4.1.5.tar.gz", hash = "sha256:ea0a113bbc111bcffc90789ebb215625c963411f7096a7e9062d4e4630c155fd"}, ] [package.dependencies] @@ -543,15 +550,36 @@ typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] +[[package]] +name = "typer" +version = "0.9.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.6" +files = [ + {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, + {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] @@ -667,4 +695,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "ac6360d9068e34f6bbad74a6c3339a85dd1968267f7272b48b8a99dfc5702812" +content-hash = "0d2735ed16d5b0a5c4cd2acee15f27f33614a589da94a7442084f88644b65e33" diff --git a/pyproject.toml b/pyproject.toml index ea4c165f..ef85dd3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + [tool.poetry] name = "regtech-data-validator" version = "0.1.0" @@ -13,15 +17,18 @@ pandera = "0.16.1" [tool.poetry.group.dev.dependencies] pytest = "7.4.0" pytest-cov = "4.1.0" -black = "23.3.0" -ruff = "0.0.259" +black = "23.10.1" +ruff = "0.1.4" [tool.poetry.group.data.dependencies] openpyxl = "^3.1.2" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.poetry.group.cli.dependencies] +tabulate = "^0.9.0" +typer = "^0.9.0" + +[tool.poetry.scripts] +cfpb-val = 'regtech_data_validator.cli:app' # Black formatting [tool.black] diff --git a/regtech_data_validator/check_functions.py b/regtech_data_validator/check_functions.py index dbbc6470..3e6157ad 100644 --- a/regtech_data_validator/check_functions.py +++ b/regtech_data_validator/check_functions.py @@ -11,7 +11,6 @@ the function. This may or may not align with the name of the validation in the fig.""" - import re from datetime import datetime, timedelta from typing import Dict diff --git a/regtech_data_validator/cli.py b/regtech_data_validator/cli.py new file mode 100644 index 00000000..976dc67c --- /dev/null +++ b/regtech_data_validator/cli.py @@ -0,0 +1,155 @@ +from dataclasses import dataclass +from enum import StrEnum +import json +from pathlib import Path +from typing import Annotated, Optional + +import pandas as pd +from tabulate import tabulate +import typer + +from regtech_data_validator.create_schemas import validate_phases + + +app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False) + + +@dataclass +class KeyValueOpt: + key: str + value: str + + +def parse_key_value(kv_str: str) -> KeyValueOpt: + split_str = kv_str.split('=') + + if len(split_str) != 2: + raise ValueError(f'Invalid key/value pair: {kv_str}') + + return KeyValueOpt(split_str[0], split_str[1]) + + +class OutputFormat(StrEnum): + CSV = 'csv' + JSON = 'json' + PANDAS = 'pandas' + TABLE = 'table' + + +def df_to_str(df: pd.DataFrame) -> str: + with pd.option_context('display.width', None, 'display.max_rows', None): + return str(df) + + +def df_to_csv(df: pd.DataFrame) -> str: + return df.to_csv() + + +def df_to_table(df: pd.DataFrame) -> str: + # trim field_value field to just 50 chars, similar to DataFrame default + table_df = df.drop(columns='validation_desc').sort_index() + table_df['field_value'] = table_df['field_value'].str[0:50] + + # NOTE: `type: ignore` because tabulate package typing does not include Pandas + # DataFrame as input, but the library itself does support it. ¯\_(ツ)_/¯ + return tabulate(table_df, headers='keys', showindex=True, tablefmt='rounded_outline') # type: ignore + + +def df_to_json(df: pd.DataFrame) -> str: + findings_json = [] + findings_by_v_id_df = df.reset_index().set_index(['validation_id', 'record_no', 'field_name']) + + for v_id_idx, v_id_df in findings_by_v_id_df.groupby(by='validation_id'): + v_head = v_id_df.iloc[0] + + finding_json = { + 'validation': { + 'id': v_id_idx, + 'name': v_head.at['validation_name'], + 'description': v_head.at['validation_desc'], + 'severity': v_head.at['validation_severity'], + }, + 'records': [], + } + findings_json.append(finding_json) + + for rec_idx, rec_df in v_id_df.groupby(by='record_no'): + record_json = {'record_no': rec_idx, 'fields': []} + finding_json['records'].append(record_json) + + for field_idx, field_df in rec_df.groupby(by='field_name'): + field_head = field_df.iloc[0] + record_json['fields'].append({'name': field_idx, 'value': field_head.at['field_value']}) + + json_str = json.dumps(findings_json, indent=4) + + return json_str + + +@app.command() +def describe() -> None: + """ + Describe CFPB data submission formats and validations + """ + + print('Feature coming soon...') + + +@app.command(no_args_is_help=True) +def validate( + path: Annotated[ + Path, + typer.Argument( + exists=True, + dir_okay=False, + readable=True, + resolve_path=True, + show_default=False, + help='Path of file to be validated', + ), + ], + context: Annotated[ + Optional[list[KeyValueOpt]], + typer.Option( + parser=parse_key_value, + metavar='=', + help='[example: lei=12345678901234567890]', + show_default=False, + ), + ] = None, + output: Annotated[Optional[OutputFormat], typer.Option()] = OutputFormat.TABLE, +) -> tuple[bool, pd.DataFrame]: + """ + Validate CFPB data submission + """ + context_dict = {x.key: x.value for x in context} if context else {} + input_df = pd.read_csv(path, dtype=str, na_filter=False) + is_valid, findings_df = validate_phases(input_df, context_dict) + + status = 'SUCCESS' + no_of_findings = 0 + + if not is_valid: + status = 'FAILURE' + no_of_findings = len(findings_df.index.unique()) + + match output: + case OutputFormat.PANDAS: + print(df_to_str(findings_df)) + case OutputFormat.CSV: + print(df_to_csv(findings_df)) + case OutputFormat.JSON: + print(df_to_json(findings_df)) + case OutputFormat.TABLE: + print(df_to_table(findings_df)) + case _: + raise ValueError(f'output format "{output}" not supported') + + typer.echo(f"status: {status}, findings: {no_of_findings}", err=True) + + # returned values are only used in unit tests + return is_valid, findings_df + + +if __name__ == '__main__': + app() diff --git a/regtech_data_validator/create_schemas.py b/regtech_data_validator/create_schemas.py index bbb5e99d..1c03e0cc 100644 --- a/regtech_data_validator/create_schemas.py +++ b/regtech_data_validator/create_schemas.py @@ -2,7 +2,7 @@ with validations listed in phase 1 and phase 2.""" import pandas as pd -from pandera import DataFrameSchema +from pandera import Check, DataFrameSchema from pandera.errors import SchemaErrors, SchemaError from regtech_data_validator.checks import SBLCheck @@ -11,43 +11,110 @@ # Get separate schema templates for phase 1 and 2 - - phase_1_template = get_template() phase_2_template = get_template() -def get_schema_by_phase_for_lei(template: dict, phase: str, lei: str | None = None): - for column in get_phase_1_and_2_validations_for_lei(lei): - validations = get_phase_1_and_2_validations_for_lei(lei)[column] +def get_schema_by_phase_for_lei(template: dict, phase: str, context: dict[str, str] | None = None): + for column in get_phase_1_and_2_validations_for_lei(context): + validations = get_phase_1_and_2_validations_for_lei(context)[column] template[column].checks = validations[phase] + return DataFrameSchema(template) -def get_phase_1_schema_for_lei(lei: str | None = None): - return get_schema_by_phase_for_lei(phase_1_template, "phase_1", lei) +def get_phase_1_schema_for_lei(context: dict[str, str] | None = None): + return get_schema_by_phase_for_lei(phase_1_template, "phase_1", context) + + +def get_phase_2_schema_for_lei(context: dict[str, str] | None = None): + return get_schema_by_phase_for_lei(phase_2_template, "phase_2", context) + + +def _get_check_fields(check: Check, primary_column: str) -> list[str]: + """ + Retrieves unique sorted list of fields associated with a given Check + """ + + field_set: set[str] = {primary_column} + + if check.groupby: + field_set.update(check.groupby) # type: ignore + + fields = sorted(list(field_set)) + + return fields + + +def _filter_valid_records(df: pd.DataFrame, check_output: pd.Series, fields: list[str]) -> pd.DataFrame: + """ + Return only records and fields associated with a given `Check`'s + """ + + # `check_output` must be sorted so its index lines up with `df`'s index + sorted_check_output: pd.Series = check_output.sort_index() + # Filter records using Pandas's boolean indexing, where all False values get filtered out. + # The `~` does the inverse since it's actually the False values we want to keep. + # http://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#boolean-indexing + failed_records_df = df[~sorted_check_output][fields].reset_index(names='record_no') + failed_records_df.index.rename('finding_no', inplace=True) -def get_phase_2_schema_for_lei(lei: str | None = None): - return get_schema_by_phase_for_lei(phase_2_template, "phase_2", lei) + return failed_records_df -def validate(schema: DataFrameSchema, df: pd.DataFrame) -> list[dict]: +def _records_to_fields(failed_records_df: pd.DataFrame) -> pd.DataFrame: + """ + Transforms a DataFrame with columns per Check field to DataFrame with a row per field + """ + + # Melts a DataFrame with the line number as the index columns for the validations's fields' values + # into one with the validation_id, record_no, and field_name as a multiindex, and all of the validation + # metadata merged in as well. + failed_record_fields_df = failed_records_df.melt( + var_name='field_name', value_name='field_value', id_vars='record_no', ignore_index=False + ) + + return failed_record_fields_df + + +def _add_validation_metadata(failed_check_fields_df: pd.DataFrame, check: SBLCheck): + """ + Add SBLCheck metadata (id, name, description, severity) + """ + + validation_fields_df = ( + failed_check_fields_df.assign(validation_severity=check.severity) + .assign(validation_id=check.title) + .assign(validation_name=check.name) + .assign(validation_desc=check.description) + ) + + return validation_fields_df + + +def validate(schema: DataFrameSchema, submission_df: pd.DataFrame) -> tuple[bool, pd.DataFrame]: """ validate received dataframe with schema and return list of schema errors Args: schema (DataFrameSchema): schema to be used for validation - df (pd.DataFrame): data parsed into dataframe + submission_df (pd.DataFrame): data to be validated against the schema Returns: - list of validation findings (warnings and errors) + bool whether the given submission was valid or not + pd.DataFrame containing validation results data """ - findings = [] + is_valid = True + findings_df: pd.DataFrame = pd.DataFrame() + next_finding_no: int = 1 + try: - schema(df, lazy=True) + schema(submission_df, lazy=True) except SchemaErrors as err: - # WARN: SchemaErrors.schema_errors is supposed to be of type - # list[dict[str,Any]], but it's actually of type SchemaError + is_valid = False + + # NOTE: `type: ignore` because SchemaErrors.schema_errors is supposed to be + # `list[dict[str,Any]]`, but it's actually of type `SchemaError` schema_error: SchemaError for schema_error in err.schema_errors: # type: ignore check = schema_error.check @@ -63,59 +130,31 @@ def validate(schema: DataFrameSchema, df: pd.DataFrame) -> list[dict]: f'Check {check} type on {column_name} column not supported. Must be of type {SBLCheck}' ) from schema_error - fields: list[str] = [column_name] + fields = _get_check_fields(check, column_name) - if check.groupby: - fields += check.groupby # type: ignore + check_output: pd.Series | None = schema_error.check_output - # This will either be a boolean series or a single bool - check_output = schema_error.check_output + if check_output is not None: + # Filter data not associated with failed Check, and update index for merging with findings_df + failed_records_df = _filter_valid_records(submission_df, check_output, fields) + failed_records_df.index += next_finding_no + next_finding_no = failed_records_df.tail(1).index + 1 # type: ignore - # Remove duplicates, but keep as `list` for JSON-friendliness - fields = list(set(fields)) + failed_record_fields_df = _records_to_fields(failed_records_df) + check_findings_df = _add_validation_metadata(failed_record_fields_df, check) - if check_output is not None: - # `check_output` must be sorted so its index lines up with `df`'s index - check_output.sort_index(inplace=True) - - # Filter records using Pandas's boolean indexing, where all False values - # get filtered out. The `~` does the inverse since it's actually the - # False values we want to keep. - # http://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#boolean-indexing - failed_check_fields_df = df[~check_output][fields].fillna("") - - # Create list of dicts representing the failed validations and the - # associated field data for each invalid record. - records = [] - for idx, row in failed_check_fields_df.iterrows(): - record = {"number": idx + 1, "field_values": {}} - for field in fields: - record["field_values"][field] = row[field] - records.append(record) - - validation_findings = { - "validation": { - "id": check.title, - "name": check.name, - "description": check.description, - "severity": check.severity, - "fields": fields, - }, - "records": records, - } - - findings.append(validation_findings) - - return findings - - -def validate_phases(df: pd.DataFrame, lei: str | None = None) -> list: - phase1_findings = validate(get_phase_1_schema_for_lei(lei), df) - if phase1_findings: - return phase1_findings - else: - phase2_findings = validate(get_phase_2_schema_for_lei((lei)), df) - if phase2_findings: - return phase2_findings - else: - return [{"response": "No validations errors or warnings"}] + findings_df = pd.concat([findings_df, check_findings_df]) + else: + # The above exception handling _should_ prevent this from ever happenin, but...just in case. + raise RuntimeError(f'No check output for "{check.name}" check. Pandera SchemaError: {schema_error}') + + return is_valid, findings_df.sort_index() + + +def validate_phases(df: pd.DataFrame, context: dict[str, str] | None = None) -> tuple[bool, pd.DataFrame]: + p1_is_valid, p1_findings = validate(get_phase_1_schema_for_lei(context), df) + + if not p1_is_valid: + return p1_is_valid, p1_findings + + return validate(get_phase_2_schema_for_lei(context), df) diff --git a/regtech_data_validator/main.py b/regtech_data_validator/main.py deleted file mode 100644 index bed8bb3a..00000000 --- a/regtech_data_validator/main.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -This script loads a given CSV into a Pandas DataFrame, and then validates it against -the SBL Pandera schema. - -Run from the terminal to see the generated output. -""" - -import json -import sys - -import pandas as pd - -from regtech_data_validator.create_schemas import validate_phases - - -def csv_to_df(path: str) -> pd.DataFrame: - return pd.read_csv(path, dtype=str, na_filter=False) - - -def run_validation_on_df(df: pd.DataFrame, lei: str | None) -> None: - """ - Run validation on the supplied dataframe and print a report to - the terminal. - """ - - validation_dict = validate_phases(df, lei) - validation_json = json.dumps(validation_dict, indent=4) - - print(validation_json) - - -def main(): - csv_path = None - lei: str | None = None - if len(sys.argv) == 1: - raise ValueError("csv_path arg not provided") - elif len(sys.argv) == 2: - csv_path = sys.argv[1] - elif len(sys.argv) == 3: - lei = sys.argv[1] - csv_path = sys.argv[2] - else: - raise ValueError("correct number of args not provided") - - df = csv_to_df(csv_path) - run_validation_on_df(df, lei) - - -if __name__ == "__main__": - main() diff --git a/regtech_data_validator/phase_validations.py b/regtech_data_validator/phase_validations.py index 20b23c06..c9271887 100644 --- a/regtech_data_validator/phase_validations.py +++ b/regtech_data_validator/phase_validations.py @@ -3,7 +3,6 @@ This mapping is used to populate the schema template object and create an instance of a PanderaSchema object for phase 1 and phase 2.""" - from regtech_data_validator import global_data from regtech_data_validator.check_functions import ( has_correct_length, @@ -31,7 +30,9 @@ from regtech_data_validator.checks import SBLCheck, Severity -def get_phase_1_and_2_validations_for_lei(lei: str | None = None): +def get_phase_1_and_2_validations_for_lei(context: dict[str, str] | None = None): + lei: str | None = context.get('lei', None) if context else None + return { "uid": { "phase_1": [ @@ -70,6 +71,8 @@ def get_phase_1_and_2_validations_for_lei(lei: str | None = None): element_wise=True, regex="^[A-Z0-9]+$", ), + ], + "phase_2": [ SBLCheck( string_contains, id="W0003", @@ -85,7 +88,6 @@ def get_phase_1_and_2_validations_for_lei(lei: str | None = None): end_idx=20, ), ], - "phase_2": [], }, "app_date": { "phase_1": [ @@ -759,7 +761,7 @@ def get_phase_1_and_2_validations_for_lei(lei: str | None = None): "phase_1": [ SBLCheck( is_valid_enum, - id="E0001", + id="E0340", name="denial_reasons.invalid_enum_value", description=( "Each value in 'denial reason(s)' (separated by semicolons)" @@ -1030,12 +1032,12 @@ def get_phase_1_and_2_validations_for_lei(lei: str | None = None): ), SBLCheck( is_greater_than, - id="E0001", + id="W0441", name="pricing_adj_margin.unreasonable_numeric_value", description=( "When present, 'adjustable rate transaction: margin' should generally be greater than 0.1." ), - severity=Severity.ERROR, + severity=Severity.WARNING, element_wise=True, min_value="0.1", accept_blank=True, @@ -1330,7 +1332,7 @@ def get_phase_1_and_2_validations_for_lei(lei: str | None = None): "phase_1": [ SBLCheck( is_valid_enum, - id="E0640", + id="E0660", name="census_tract_adr_type.invalid_enum_value", description="'Census tract: type of address' must equal 1, 2, 3, or 988.", severity=Severity.ERROR, @@ -1443,7 +1445,7 @@ def get_phase_1_and_2_validations_for_lei(lei: str | None = None): "phase_1": [ SBLCheck( is_valid_enum, - id="E0720", + id="E0740", name="naics_code_flag.invalid_enum_value", description=( "'North American Industry Classification System (NAICS) code: NP flag'must equal 900 or 988." diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..72759363 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,230 @@ +from pathlib import Path +from textwrap import dedent +import os + +import pandas as pd +import pytest +from typer.testing import CliRunner + +from regtech_data_validator import cli + +cli_runner = CliRunner(mix_stderr=False) +data_dir = f'{os.path.dirname(os.path.realpath(__file__))}/data' +pass_file = f'{data_dir}/sbl-validations-pass.csv' +fail_file = f'{data_dir}/sbl-validations-fail.csv' + + +class TestParseKeyValue: + def test_parse_success(self): + test_str = "fruit=apple" + key_val: cli.KeyValueOpt = cli.parse_key_value(test_str) + + assert key_val.key == 'fruit' + assert key_val.value == 'apple' + + def test_parse_fail_wrong_delimiter(self): + test_str = "fruit:apple" + + with pytest.raises(ValueError): + cli.parse_key_value(test_str) + + def test_parse_fail_no_delimiter(self): + test_str = "fruitapple" + + with pytest.raises(ValueError): + cli.parse_key_value(test_str) + + def test_parse_fail_multiple_delimiters(self): + test_str = "fruit=apple=orange" + + with pytest.raises(ValueError): + cli.parse_key_value(test_str) + + +class TestOutputFormat: + # TODO: Figure out why uid.duplicates_in_dataset returns different findings for matched records + input_df = pd.DataFrame( + data=[ + { + 'record_no': 1, + 'field_name': 'uid', + 'field_value': '12345678901234567890', + 'validation_severity': 'error', + 'validation_id': 'E3000', + 'validation_name': 'uid.duplicates_in_dataset', + 'validation_desc': "Any 'unique identifier' may not be used in mor...", + }, + { + 'record_no': 2, + 'field_name': 'uid', + 'field_value': '12345678901234567890', + 'validation_severity': 'error', + 'validation_id': 'E3000', + 'validation_name': 'uid.duplicates_in_dataset', + 'validation_desc': "Any 'unique identifier' may not be used in mor...", + }, + ], + ) + input_df.index.name = 'finding_no' + input_df.index += 1 + + def test_output_pandas(self): + expected_output = dedent(""" + record_no field_name field_value validation_severity validation_id validation_name validation_desc + finding_no + 1 1 uid 12345678901234567890 error E3000 uid.duplicates_in_dataset Any 'unique identifier' may not be used in mor... + 2 2 uid 12345678901234567890 error E3000 uid.duplicates_in_dataset Any 'unique identifier' may not be used in mor... + """).strip('\n') # noqa: E501 + + actual_output = cli.df_to_str(self.input_df) + + assert actual_output == expected_output + + def test_output_table(self): + expected_output = dedent(""" + ╭──────────────┬─────────────┬──────────────┬──────────────────────┬───────────────────────┬─────────────────┬───────────────────────────╮ + │ finding_no │ record_no │ field_name │ field_value │ validation_severity │ validation_id │ validation_name │ + ├──────────────┼─────────────┼──────────────┼──────────────────────┼───────────────────────┼─────────────────┼───────────────────────────┤ + │ 1 │ 1 │ uid │ 12345678901234567890 │ error │ E3000 │ uid.duplicates_in_dataset │ + │ 2 │ 2 │ uid │ 12345678901234567890 │ error │ E3000 │ uid.duplicates_in_dataset │ + ╰──────────────┴─────────────┴──────────────┴──────────────────────┴───────────────────────┴─────────────────┴───────────────────────────╯ + """).strip('\n') # noqa: E501 + + actual_output = cli.df_to_table(self.input_df) + + assert actual_output == expected_output + + def test_output_csv(self): + expected_output = dedent(""" + finding_no,record_no,field_name,field_value,validation_severity,validation_id,validation_name,validation_desc + 1,1,uid,12345678901234567890,error,E3000,uid.duplicates_in_dataset,Any 'unique identifier' may not be used in mor... + 2,2,uid,12345678901234567890,error,E3000,uid.duplicates_in_dataset,Any 'unique identifier' may not be used in mor... + """).strip('\n') # noqa: E501 + + actual_output = cli.df_to_csv(self.input_df) + + assert actual_output.strip('\n') == expected_output + + def test_output_json(self): + expected_output = dedent(""" + [ + { + "validation": { + "id": "E3000", + "name": "uid.duplicates_in_dataset", + "description": "Any 'unique identifier' may not be used in mor...", + "severity": "error" + }, + "records": [ + { + "record_no": 1, + "fields": [ + { + "name": "uid", + "value": "12345678901234567890" + } + ] + }, + { + "record_no": 2, + "fields": [ + { + "name": "uid", + "value": "12345678901234567890" + } + ] + } + ] + } + ] + """).strip('\n') + + actual_output = cli.df_to_json(self.input_df) + + assert actual_output == expected_output + + +class TestDescribeCommand: + def test_defaults(self): + cli.describe() + + +class TestValidateCommand: + valid_lei_context = cli.KeyValueOpt('lei', '000TESTFIUIDDONOTUSE') + invalid_lei_context = cli.KeyValueOpt('lei', 'XXXXXXXXXXXXXXXXXXXX') + + pass_path = Path(pass_file) + fail_path = Path(fail_file) + + def test_pass_file_defaults(self): + is_valid, findings_df = cli.validate(path=self.pass_path) + + assert is_valid + + def test_pass_file_with_valid_context(self): + is_valid, findings_df = cli.validate(path=self.pass_path, context=[self.valid_lei_context]) + + assert is_valid + + def test_pass_file_with_invalid_context(self): + is_valid, findings_df = cli.validate(path=self.pass_path, context=[self.invalid_lei_context]) + + assert not is_valid + + def test_fail_file_csv_output(self): + is_valid, findings_df = cli.validate(path=self.fail_path, output=cli.OutputFormat.CSV) + + assert not is_valid + + def test_fail_file_json_output(self): + is_valid, findings_df = cli.validate(path=self.fail_path, output=cli.OutputFormat.JSON) + + assert not is_valid + + def test_fail_file_pandas_output(self): + is_valid, findings_df = cli.validate(path=self.fail_path, output=cli.OutputFormat.PANDAS) + + assert not is_valid + + def test_fail_file_table_output(self): + is_valid, findings_df = cli.validate(path=self.fail_path, output=cli.OutputFormat.TABLE) + + assert not is_valid + + +class TestDescribeCli: + """ + Test `describe` command with Typer's CLI test runner + """ + + def test_defaults(self): + result = cli_runner.invoke(cli.app, ['describe']) + + assert result.exit_code == 0 + assert result.stdout == 'Feature coming soon...\n' + + +class TestValidateCli: + """ + Test `validate` command with Typer's CLI test runner + """ + + def test_pass_file_defaults(self): + result = cli_runner.invoke(cli.app, ['validate', pass_file]) + + assert result.exit_code == 0 + assert result.stdout == '' + assert result.stderr == 'status: SUCCESS, findings: 0\n' + + def test_pass_file_invalid_output_arg_value(self): + result = cli_runner.invoke(cli.app, ['validate', pass_file, '--output', 'pdf']) + + assert result.exit_code == 2 + assert "Invalid value for '--output': 'pdf' is not one of" in result.stderr + + def test_fail_file_defaults(self): + result = cli_runner.invoke(cli.app, ['validate', fail_file]) + + assert result.exit_code == 0 + assert result.stdout != '' + assert 'status: FAILURE, findings:' in result.stderr diff --git a/tests/test_sample_data.py b/tests/test_sample_data.py index 35c1fe42..4a760e4a 100644 --- a/tests/test_sample_data.py +++ b/tests/test_sample_data.py @@ -8,8 +8,6 @@ class TestValidatingSampleData: - valid_response = {"response": "No validations errors or warnings"} - good_file_df = pd.read_csv(GOOD_FILE_PATH, dtype=str, na_filter=False) bad_file_df = pd.read_csv(BAD_FILE_PATH, dtype=str, na_filter=False) @@ -21,28 +19,36 @@ def test_invalid_data_file(self): def test_run_validation_on_good_data_invalid_lei(self): lei = "000TESTFIUIDDONOTUS1" - validation_result = validate_phases(self.good_file_df, lei) + is_valid, findings_df = validate_phases(self.good_file_df, {'lei': lei}) + + assert not is_valid - assert len(validation_result) == 1 - assert validation_result[0] != self.valid_response + # Only 'uid.invalid_uid_lei' validation returned + assert len(findings_df['validation_name'].unique()) == 1 + assert len(findings_df['validation_name'] == 'uid.invalid_uid_lei') > 0 def test_run_validation_on_good_data_valid_lei(self): lei = "000TESTFIUIDDONOTUSE" - validation_result = validate_phases(self.good_file_df, lei) + is_valid, findings_df = validate_phases(self.good_file_df, {'lei': lei}) - assert len(validation_result) == 1 - assert validation_result[0] == self.valid_response + assert is_valid + assert findings_df.empty def test_run_validation_on_bad_data_invalid_lei(self): lei = "000TESTFIUIDDONOTUS1" - validation_result = validate_phases(self.bad_file_df, lei) + is_valid, findings_df = validate_phases(self.bad_file_df, {'lei': lei}) - assert len(validation_result) >= 1 - assert validation_result[0] != self.valid_response + assert not is_valid + + # 'uid.invalid_uid_lei' and other validations returned + assert len(findings_df['validation_name'].unique()) > 1 + assert len(findings_df['validation_name'] == 'uid.invalid_uid_lei') > 0 def test_run_validation_on_bad_data_valid_lei(self): lei = "000TESTFIUIDDONOTUSE" - validation_result = validate_phases(self.bad_file_df, lei) + is_valid, findings_df = validate_phases(self.bad_file_df, {'lei': lei}) + + assert not is_valid - assert len(validation_result) >= 1 - assert validation_result[0] != self.valid_response + # 'uid.invalid_uid_lei' and other validations returned + assert len(findings_df['validation_name'].unique()) > 1 diff --git a/tests/test_schema_functions.py b/tests/test_schema_functions.py index 7c141dee..bc8ae363 100644 --- a/tests/test_schema_functions.py +++ b/tests/test_schema_functions.py @@ -9,8 +9,6 @@ class TestUtil: - valid_response = {"response": "No validations errors or warnings"} - def get_data(self, update_data: dict[str, list[str]] = {}) -> dict[str, list[str]]: default = { "uid": ["000TESTFIUIDDONOTUSEXGXVID11XTC1"], @@ -106,27 +104,34 @@ class TestValidate: def test_with_valid_dataframe(self): df = pd.DataFrame(data=self.util.get_data()) - result = validate(self.phase1_schema, df) - ph2_result = validate(self.phase2_schema, df) - assert len(result) == 0 - assert len(ph2_result) == 0 + p1_is_valid, p1_findings_df = validate(self.phase1_schema, df) + p2_is_valid, p2_findings_df = validate(self.phase2_schema, df) + + assert p1_is_valid + assert p2_is_valid def test_with_valid_lei(self): lei = "000TESTFIUIDDONOTUSE" - phase1_schema_by_lei = get_phase_1_schema_for_lei(lei) - phase2_schema_by_lei = get_phase_2_schema_for_lei(lei) + phase1_schema_by_lei = get_phase_1_schema_for_lei({'lei': lei}) + phase2_schema_by_lei = get_phase_2_schema_for_lei({'lei': lei}) + df = pd.DataFrame(data=self.util.get_data()) - result = validate(phase1_schema_by_lei, df) - ph2_result = validate(phase2_schema_by_lei, df) - assert len(result) == 0 - assert len(ph2_result) == 0 + + p1_is_valid, p1_findings_df = validate(phase1_schema_by_lei, df) + p2_is_valid, p2_findings_df = validate(phase2_schema_by_lei, df) + + assert p1_is_valid + assert p2_is_valid def test_with_invalid_dataframe(self): df = pd.DataFrame(data=self.util.get_data({"ct_credit_product": ["989"]})) - result = validate(self.phase1_schema, df) - ph2_result = validate(self.phase2_schema, df) - assert len(result) == 1 - assert len(ph2_result) == 0 + + p1_is_valid, p1_findings_df = validate(self.phase1_schema, df) + p2_is_valid, p2_findings_df = validate(self.phase2_schema, df) + + assert not p1_is_valid + assert len(p1_findings_df) == 1 + assert p2_is_valid def test_with_multi_invalid_dataframe(self): df = pd.DataFrame( @@ -138,47 +143,57 @@ def test_with_multi_invalid_dataframe(self): } ) ) - result = validate(self.phase1_schema, df) - assert len(result) == 1 + p1_is_valid, p1_findings_df = validate(self.phase1_schema, df) + assert not p1_is_valid + assert len(p1_findings_df) == 1 - ph2_result = validate(self.phase2_schema, df) - assert len(ph2_result) == 3 + p2_is_valid, p2_findings_df = validate(self.phase2_schema, df) + # 3 unique findings raised + assert len(p2_findings_df.index.unique()) == 3 def test_with_invalid_lei(self): lei = "000TESTFIUIDDONOTUS1" - phase1_schema_by_lei = get_phase_1_schema_for_lei(lei) - phase2_schema_by_lei = get_phase_2_schema_for_lei(lei) + + phase1_schema_by_lei = get_phase_1_schema_for_lei({'lei': lei}) + phase2_schema_by_lei = get_phase_2_schema_for_lei({'lei': lei}) + df = pd.DataFrame(data=self.util.get_data({"ct_credit_product": ["989"]})) - result = validate(phase1_schema_by_lei, df) - ph2_result = validate(phase2_schema_by_lei, df) - assert len(result) == 2 - assert len(ph2_result) == 0 + + p1_is_valid, p1_findings_df = validate(phase1_schema_by_lei, df) + p2_is_valid, p2_findings_df = validate(phase2_schema_by_lei, df) + + # 1 unique findings raised in phase 1 + assert not p1_is_valid + assert len(p1_findings_df.index.unique()) == 1 + + # 1 unique finding raised in phase 2 + assert not p2_is_valid + assert len(p2_findings_df.index.unique()) == 1 class TestValidatePhases: util = TestUtil() def test_with_valid_data(self): - result = validate_phases(pd.DataFrame(data=self.util.get_data())) + is_valid, findings_df = validate_phases(pd.DataFrame(data=self.util.get_data())) - assert len(result) == 1 - assert result[0] == self.util.valid_response + assert is_valid def test_with_valid_lei(self): lei = "000TESTFIUIDDONOTUSE" df = pd.DataFrame(data=self.util.get_data()) - result = validate_phases(df, lei) - assert len(result) == 1 - assert result[0] == self.util.valid_response + is_valid, findings_df = validate_phases(df, {'lei': lei}) + + assert is_valid def test_with_invalid_data(self): - result = validate_phases(pd.DataFrame(data=self.util.get_data({"ct_credit_product": ["989"]}))) + is_valid, findings_df = validate_phases(pd.DataFrame(data=self.util.get_data({"ct_credit_product": ["989"]}))) - assert len(result) == 1 - assert result[0] != self.util.valid_response + assert not is_valid + assert len(findings_df) == 1 def test_with_multi_invalid_data_with_phase1(self): - result = validate_phases( + is_valid, findings_df = validate_phases( pd.DataFrame( data=self.util.get_data( { @@ -189,12 +204,13 @@ def test_with_multi_invalid_data_with_phase1(self): ) ) ) + # should only return phase 1 validation result since phase1 failed - assert len(result) == 1 - assert result[0] != self.util.valid_response + assert not is_valid + assert len(findings_df) == 1 def test_with_multi_invalid_data_with_phase2(self): - result = validate_phases( + is_valid, findings_df = validate_phases( pd.DataFrame( data=self.util.get_data( { @@ -206,12 +222,13 @@ def test_with_multi_invalid_data_with_phase2(self): ) # since the data passed phase 1 validations # this should return phase 2 validations - assert len(result) == 3 - assert result[0] != self.util.valid_response + assert not is_valid + assert len(findings_df.index.unique()) == 3 def test_with_invalid_lei(self): lei = "000TESTFIUIDDONOTUS1" df = pd.DataFrame(data=self.util.get_data()) - result = validate_phases(df, lei) - assert len(result) == 1 - assert result[0] != self.util.valid_response + is_valid, findings_df = validate_phases(df, {'lei': lei}) + + assert not is_valid + assert len(findings_df['validation_name'] == 'uid.invalid_uid_lei') > 0