Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

140 validation metadata #165

Merged
merged 16 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/sbl_filing_api/entities/models/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class SubmissionDAO(Base):
accepter: Mapped[UserActionDAO] = relationship(lazy="selectin", foreign_keys=[accepter_id])
state: Mapped[SubmissionState] = mapped_column(SAEnum(SubmissionState))
validation_ruleset_version: Mapped[str] = mapped_column(nullable=True)
validation_json: Mapped[List[dict[str, Any]]] = mapped_column(JSON, nullable=True)
validation_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=True)
submission_time: Mapped[datetime] = mapped_column(server_default=func.now())
filename: Mapped[str]

Expand Down
2 changes: 1 addition & 1 deletion src/sbl_filing_api/entities/models/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class SubmissionDTO(BaseModel):
id: int | None = None
state: SubmissionState | None = None
validation_ruleset_version: str | None = None
validation_json: List[Dict[str, Any]] | None = None
validation_json: Dict[str, Any] | None = None
submission_time: datetime | None = None
filename: str
submitter: UserActionDTO
Expand Down
29 changes: 26 additions & 3 deletions src/sbl_filing_api/services/submission_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from io import BytesIO
from fastapi import UploadFile
from regtech_data_validator.create_schemas import validate_phases
from regtech_data_validator.create_schemas import validate_phases, ValidationPhase
from regtech_data_validator.data_formatters import df_to_json, df_to_download
from regtech_data_validator.checks import Severity
import pandas as pd
Expand Down Expand Up @@ -101,7 +101,7 @@ async def validate_and_update_submission(period_code: str, lei: str, submission:
)
else:
submission.state = SubmissionState.VALIDATION_SUCCESSFUL
submission.validation_json = json.loads(df_to_json(result[1]))
submission.validation_json = build_validation_results(result)
submission_report = df_to_download(result[1])
await upload_to_storage(
period_code, lei, str(submission.id) + REPORT_QUALIFIER, submission_report.encode("utf-8")
Expand All @@ -112,7 +112,6 @@ async def validate_and_update_submission(period_code: str, lei: str, submission:
log.error("The file is malformed", re, exc_info=True, stack_info=True)
submission.state = SubmissionState.SUBMISSION_UPLOAD_MALFORMED
await update_submission(submission)

except Exception as e:
log.error(
f"Validation for submission {submission.id} did not complete due to an unexpected error.",
Expand All @@ -122,3 +121,27 @@ async def validate_and_update_submission(period_code: str, lei: str, submission:
)
submission.state = SubmissionState.VALIDATION_ERROR
await update_submission(submission)


def build_validation_results(result):
val_json = json.loads(df_to_json(result[1]))
val_res = {}

if result[2] == ValidationPhase.SYNTACTICAL.value:
val_res["syntax_errors"]["details"] = val_json
val_res["syntax_errors"]["count"] = len(val_res["syntax_errors"]["details"])
Copy link
Contributor

@jcadam14 jcadam14 Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like below, this section can be shortened to just:
val_res = {"syntax_errors": {"count": len(val_json), "details": val_json}}

else:
val_res = {
"syntax_errors": {"count": 0, "details": []},
"logic_errors": {"count": 0, "details": []},
"logic_warnings": {"count": 0, "details": []},
}
for v in val_json:
if v["validation"]["severity"] == Severity.WARNING.value:
val_res["logic_warnings"]["details"].append(v)
elif v["validation"]["severity"] == Severity.ERROR.value:
val_res["logic_errors"]["details"].append(v)
lchen-2101 marked this conversation as resolved.
Show resolved Hide resolved
val_res["logic_warnings"]["count"] = len(val_res["logic_warnings"]["details"])
val_res["logic_errors"]["count"] = len(val_res["logic_errors"]["details"])

return val_res
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This else block can be even further refined to:

        errors_list = [e for e in val_json if e["validation"]["severity"] == Severity.ERROR.value]
        warnings_list = [e for e in val_json if e["validation"]["severity"] == Severity.WARNING.value]
        val_res = {
            "syntax_errors": {"count": 0, "details": []},
            "logic_errors": {"count": len(errors_list), "details": errors_list},
            "logic_warnings": {"count": len(warnings_list), "details": warnings_list},
        }

to avoid looping and appending. List comprehension in python is faster on large data sets.

96 changes: 93 additions & 3 deletions tests/services/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,99 @@ def warning_submission_mock(mocker: MockerFixture, validate_submission_mock: Moc


@pytest.fixture(scope="function")
def df_to_json_mock(mocker: MockerFixture, validate_submission_mock: Mock):
mock_json_formatting = mocker.patch("sbl_filing_api.services.submission_processor.df_to_json")
mock_json_formatting.return_value = "[{}]"
def validation_success_mock(mocker: MockerFixture, validate_submission_mock: Mock):
mock_json_formatting = mocker.patch("sbl_filing_api.services.submission_processor.build_validation_results")
mock_json_formatting.return_value = """
{
"syntax_errors": {
"count": 0,
"details": []
},
"logic_errors": {
"count": 0,
"details": []
},
"logic_warnings": {
"count": 0,
"details": []
}
}"""
return mock_json_formatting


@pytest.fixture(scope="function")
def validation_syntax_errors_mock(mocker: MockerFixture, validate_submission_mock: Mock):
mock_json_formatting = mocker.patch("sbl_filing_api.services.submission_processor.build_validation_results")
mock_json_formatting.return_value = """
{
"syntax_errors": {
"count": 2,
"details": []
}
}"""
return mock_json_formatting


@pytest.fixture(scope="function")
def validation_logic_warnings_mock(mocker: MockerFixture, validate_submission_mock: Mock):
mock_json_formatting = mocker.patch("sbl_filing_api.services.submission_processor.build_validation_results")
mock_json_formatting.return_value = """
{
"syntax_errors": {
"count": 0,
"details": []
},
"logic_errors": {
"count": 0,
"details": []
},
"logic_warnings": {
"count": 1,
"details": []
}
}"""
return mock_json_formatting


@pytest.fixture(scope="function")
def validation_logic_errors_mock(mocker: MockerFixture, validate_submission_mock: Mock):
mock_json_formatting = mocker.patch("sbl_filing_api.services.submission_processor.build_validation_results")
mock_json_formatting.return_value = """
{
"syntax_errors": {
"count": 0,
"details": []
},
"logic_errors": {
"count": 4,
"details": []
},
"logic_warnings": {
"count": 0,
"details": []
}
}"""
return mock_json_formatting


@pytest.fixture(scope="function")
def validation_logic_warnings_and_errors_mock(mocker: MockerFixture, validate_submission_mock: Mock):
mock_json_formatting = mocker.patch("sbl_filing_api.services.submission_processor.build_validation_results")
mock_json_formatting.return_value = """
{
"syntax_errors": {
"count": 0,
"details": []
},
"logic_errors": {
"count": 3,
"details": []
},
"logic_warnings": {
"count": 2,
"details": []
}
}"""
return mock_json_formatting
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mocking out the return value for the method you are trying to test isn't productive; you are basically bypassing testing method, which is why the test coverage remains unchanged; if you click on the link with the lines in the test coverage comment, it shows the whole method is uncovered.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally, you would be constructing the dataframe to pass to the method for testing, but if that's too much of a lift, something you can mock for example could be df_to_json()



Expand Down
98 changes: 93 additions & 5 deletions tests/services/test_submission_processor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import asyncio
import json

import pytest

from http import HTTPStatus
Expand Down Expand Up @@ -107,7 +109,11 @@ def test_file_not_supported_file_size_too_large(self, mock_upload_file: Mock):
assert e.value.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE

async def test_validate_and_update_successful(
self, mocker: MockerFixture, successful_submission_mock: Mock, df_to_json_mock: Mock, df_to_download_mock: Mock
self,
mocker: MockerFixture,
successful_submission_mock: Mock,
validation_success_mock: Mock,
df_to_download_mock: Mock,
):
mock_sub = SubmissionDAO(
id=1,
Expand All @@ -126,12 +132,20 @@ async def test_validate_and_update_successful(
"1" + submission_processor.REPORT_QUALIFIER,
encoded_results,
)
json_results = json.loads(successful_submission_mock.mock_calls[1].args[0].validation_json)
assert successful_submission_mock.mock_calls[0].args[0].state == SubmissionState.VALIDATION_IN_PROGRESS
assert successful_submission_mock.mock_calls[0].args[0].validation_ruleset_version == "0.1.0"
assert successful_submission_mock.mock_calls[1].args[0].state == "VALIDATION_SUCCESSFUL"
assert json_results["syntax_errors"]["count"] == 0
assert json_results["logic_errors"]["count"] == 0
assert json_results["logic_warnings"]["count"] == 0

async def test_validate_and_update_warnings(
self, mocker: MockerFixture, warning_submission_mock: Mock, df_to_json_mock: Mock, df_to_download_mock: Mock
async def test_validate_and_update_logic_warnings(
self,
mocker: MockerFixture,
warning_submission_mock: Mock,
validation_logic_warnings_mock: Mock,
df_to_download_mock: Mock,
):
mock_sub = SubmissionDAO(
id=1,
Expand All @@ -149,12 +163,82 @@ async def test_validate_and_update_warnings(
"1" + submission_processor.REPORT_QUALIFIER,
encoded_results,
)
json_results = json.loads(warning_submission_mock.mock_calls[1].args[0].validation_json)
assert warning_submission_mock.mock_calls[0].args[0].state == SubmissionState.VALIDATION_IN_PROGRESS
assert warning_submission_mock.mock_calls[0].args[0].validation_ruleset_version == "0.1.0"
assert warning_submission_mock.mock_calls[1].args[0].state == "VALIDATION_WITH_WARNINGS"
assert json_results["syntax_errors"]["count"] == 0
assert json_results["logic_errors"]["count"] == 0
assert json_results["logic_warnings"]["count"] > 0

async def test_validate_and_update_syntax_errors(
self,
mocker: MockerFixture,
error_submission_mock: Mock,
validation_syntax_errors_mock: Mock,
df_to_download_mock: Mock,
):
mock_sub = SubmissionDAO(
id=1,
filing=1,
state=SubmissionState.SUBMISSION_UPLOADED,
filename="submission.csv",
)

file_mock = mocker.patch("sbl_filing_api.services.submission_processor.upload_to_storage")

async def test_validate_and_update_errors(
self, mocker: MockerFixture, error_submission_mock: Mock, df_to_json_mock: Mock, df_to_download_mock: Mock
await submission_processor.validate_and_update_submission("2024", "123456790", mock_sub, None)
encoded_results = df_to_download_mock.return_value.encode("utf-8")
assert file_mock.mock_calls[0].args == (
"2024",
"123456790",
"1" + submission_processor.REPORT_QUALIFIER,
encoded_results,
)
json_results = json.loads(error_submission_mock.mock_calls[1].args[0].validation_json)
assert error_submission_mock.mock_calls[0].args[0].state == SubmissionState.VALIDATION_IN_PROGRESS
assert error_submission_mock.mock_calls[0].args[0].validation_ruleset_version == "0.1.0"
assert error_submission_mock.mock_calls[1].args[0].state == "VALIDATION_WITH_ERRORS"
assert json_results["syntax_errors"]["count"] > 0

async def test_validate_and_update_logic_errors(
self,
mocker: MockerFixture,
error_submission_mock: Mock,
validation_logic_errors_mock: Mock,
df_to_download_mock: Mock,
):
mock_sub = SubmissionDAO(
id=1,
filing=1,
state=SubmissionState.SUBMISSION_UPLOADED,
filename="submission.csv",
)

file_mock = mocker.patch("sbl_filing_api.services.submission_processor.upload_to_storage")

await submission_processor.validate_and_update_submission("2024", "123456790", mock_sub, None)
encoded_results = df_to_download_mock.return_value.encode("utf-8")
assert file_mock.mock_calls[0].args == (
"2024",
"123456790",
"1" + submission_processor.REPORT_QUALIFIER,
encoded_results,
)
json_results = json.loads(error_submission_mock.mock_calls[1].args[0].validation_json)
assert error_submission_mock.mock_calls[0].args[0].state == SubmissionState.VALIDATION_IN_PROGRESS
assert error_submission_mock.mock_calls[0].args[0].validation_ruleset_version == "0.1.0"
assert error_submission_mock.mock_calls[1].args[0].state == "VALIDATION_WITH_ERRORS"
assert json_results["syntax_errors"]["count"] == 0
assert json_results["logic_errors"]["count"] > 0
assert json_results["logic_warnings"]["count"] == 0

async def test_validate_and_update_logic_warnings_and_errors(
self,
mocker: MockerFixture,
error_submission_mock: Mock,
validation_logic_warnings_and_errors_mock: Mock,
df_to_download_mock: Mock,
):
mock_sub = SubmissionDAO(
id=1,
Expand All @@ -173,9 +257,13 @@ async def test_validate_and_update_errors(
"1" + submission_processor.REPORT_QUALIFIER,
encoded_results,
)
json_results = json.loads(error_submission_mock.mock_calls[1].args[0].validation_json)
assert error_submission_mock.mock_calls[0].args[0].state == SubmissionState.VALIDATION_IN_PROGRESS
assert error_submission_mock.mock_calls[0].args[0].validation_ruleset_version == "0.1.0"
assert error_submission_mock.mock_calls[1].args[0].state == "VALIDATION_WITH_ERRORS"
assert json_results["syntax_errors"]["count"] == 0
assert json_results["logic_errors"]["count"] > 0
assert json_results["logic_warnings"]["count"] > 0

async def test_validate_and_update_submission_malformed(
self,
Expand Down