From 418ec7d9c9fb6b0bbc89bba38563cf0156526628 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 1 May 2023 15:26:29 -0400 Subject: [PATCH] updated typeguard and fixed issues w/ burnettk --- spiffworkflow-backend/conftest.py | 2 +- spiffworkflow-backend/poetry.lock | 18 +++-- spiffworkflow-backend/pyproject.toml | 2 +- .../models/process_instance_report.py | 81 ------------------- .../routes/process_instances_controller.py | 2 +- .../routes/process_models_controller.py | 2 +- .../process_instance_report_service.py | 74 +++++++++-------- .../integration/test_process_api.py | 43 +++++++--- 8 files changed, 87 insertions(+), 137 deletions(-) diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index df002ff49..45e9fd54d 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -23,7 +23,7 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer # We need to call this before importing spiffworkflow_backend # otherwise typeguard cannot work. hence the noqa: E402 if os.environ.get("RUN_TYPEGUARD") == "true": - from typeguard.importhook import install_import_hook + from typeguard import install_import_hook install_import_hook(packages="spiffworkflow_backend") diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 53a5f1f8d..9c0a426d7 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -3502,19 +3502,23 @@ files = [ [[package]] name = "typeguard" -version = "2.13.3" +version = "3.0.2" description = "Run-time type checker for Python" category = "dev" optional = false -python-versions = ">=3.5.3" +python-versions = ">=3.7.4" files = [ - {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, - {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, + {file = "typeguard-3.0.2-py3-none-any.whl", hash = "sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e"}, + {file = "typeguard-3.0.2.tar.gz", hash = "sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a"}, ] +[package.dependencies] +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.11\""} + [package.extras] -doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["mypy", "pytest", "typing-extensions"] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy (>=0.991)", "pytest (>=7)"] [[package]] name = "types-click" @@ -3934,4 +3938,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "994c36ab39238500b4fd05bc1ccdd2d729dd5f66749ab77b1028371147bdf753" +content-hash = "53f3340f73de770b4fbebff3fcd396cdf1bc2c082b929ade350f31a9df6c3860" diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index d4288f5b7..b8ce91156 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -93,7 +93,7 @@ pytest = "^7.1.2" coverage = {extras = ["toml"], version = "^6.1"} safety = "^2.3.1" mypy = ">=0.961" -typeguard = "^2" +typeguard = "^3" xdoctest = {extras = ["colors"], version = "^1.0.1"} sphinx = "^5.0.2" sphinx-autobuild = ">=2021.3.14" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index 79ff0adfa..8ad87ea1e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -5,13 +5,11 @@ import sys import typing from dataclasses import dataclass from typing import Any -from typing import cast if sys.version_info < (3, 11): from typing_extensions import TypedDict, NotRequired else: from typing import TypedDict, NotRequired -from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship @@ -19,11 +17,7 @@ from sqlalchemy.orm import relationship from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.json_data import JsonDataModel # noqa: F401 -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.services.process_instance_processor import ( - ProcessInstanceProcessor, -) class FilterValue(TypedDict): @@ -149,78 +143,3 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): if isinstance(value, str) or isinstance(value, int): field_value = str(field_value).replace("{{" + key + "}}", str(value)) return field_value - - # modeled after https://github.com/suyash248/sqlalchemy-json-querybuilder - # just supports "equals" operator for now. - # perhaps we will use the database instead of filtering in memory in the future and then we might use this lib directly. - def passes_filter(self, process_instance_dict: dict, substitution_variables: dict) -> bool: - """Passes_filter.""" - if "filter_by" in self.report_metadata: - for filter_by in self.report_metadata["filter_by"]: - field_name = filter_by["field_name"] - operator = filter_by["operator"] - field_value = self.with_substitutions(filter_by["field_value"], substitution_variables) - if operator == "equals": - if str(process_instance_dict.get(field_name)) != str(field_value): - return False - - return True - - def order_things(self, process_instance_dicts: list) -> list: - """Order_things.""" - order_by = self.report_metadata["order_by"] - - def order_by_function_for_lambda( - process_instance_dict: dict, - ) -> list[Reversor | str | None]: - """Order_by_function_for_lambda.""" - comparison_values: list[Reversor | str | None] = [] - for order_by_item in order_by: - if order_by_item.startswith("-"): - # remove leading - from order_by_item - order_by_item = order_by_item[1:] - sort_value = process_instance_dict.get(order_by_item) - comparison_values.append(Reversor(sort_value)) - else: - sort_value = cast(Optional[str], process_instance_dict.get(order_by_item)) - comparison_values.append(sort_value) - return comparison_values - - return sorted(process_instance_dicts, key=order_by_function_for_lambda) - - def generate_report( - self, - process_instances: list[ProcessInstanceModel], - substitution_variables: dict | None, - ) -> ProcessInstanceReportResult: - """Generate_report.""" - if substitution_variables is None: - substitution_variables = {} - - def to_serialized(process_instance: ProcessInstanceModel) -> dict: - """To_serialized.""" - processor = ProcessInstanceProcessor(process_instance) - process_instance.data = processor.get_current_data() - return process_instance.serialized_flat - - process_instance_dicts = map(to_serialized, process_instances) - results = [] - for process_instance_dict in process_instance_dicts: - if self.passes_filter(process_instance_dict, substitution_variables): - results.append(process_instance_dict) - - if "order_by" in self.report_metadata: - results = self.order_things(results) - - if "columns" in self.report_metadata: - column_keys_to_keep = [c["accessor"] for c in self.report_metadata["columns"]] - - pruned_results = [] - for result in results: - dict_you_want = { - your_key: result[your_key] for your_key in column_keys_to_keep if result.get(your_key) - } - pruned_results.append(dict_you_want) - results = pruned_results - - return ProcessInstanceReportResult(report_metadata=self.report_metadata, results=results) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py index dab93e8a4..fa8f62de1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instances_controller.py @@ -692,7 +692,7 @@ def _find_process_instance_for_me_or_raise( process_instance_id: int, ) -> ProcessInstanceModel: """_find_process_instance_for_me_or_raise.""" - process_instance: ProcessInstanceModel = ( + process_instance: Optional[ProcessInstanceModel] = ( ProcessInstanceModel.query.filter_by(id=process_instance_id) .outerjoin(HumanTaskModel) .outerjoin( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index 192e7f112..880cabda7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -438,7 +438,7 @@ def process_model_create_with_natural_language( def _get_file_from_request() -> FileStorage: """Get_file_from_request.""" - request_file: FileStorage = connexion.request.files.get("file") + request_file: Optional[FileStorage] = connexion.request.files.get("file") if not request_file: raise ApiError( error_code="no_file_given", diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index 7f67051e2..91f359384 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -53,98 +53,105 @@ class ProcessInstanceReportService: } system_report_completed_instances_initiated_by_me: ReportMetadata = { "columns": [ - {"Header": "id", "accessor": "id"}, + {"Header": "id", "accessor": "id", "filterable": False}, { "Header": "process_model_display_name", "accessor": "process_model_display_name", + "filterable": False, }, - {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, - {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, - {"Header": "status", "accessor": "status"}, + {"Header": "start_in_seconds", "accessor": "start_in_seconds", "filterable": False}, + {"Header": "end_in_seconds", "accessor": "end_in_seconds", "filterable": False}, + {"Header": "status", "accessor": "status", "filterable": False}, ], "filter_by": [ - {"field_name": "initiated_by_me", "field_value": True}, - {"field_name": "process_status", "field_value": terminal_status_values}, + {"field_name": "initiated_by_me", "field_value": True, "operator": "equals"}, + {"field_name": "process_status", "field_value": terminal_status_values, "operator": "equals"}, ], "order_by": ["-start_in_seconds", "-id"], } system_report_completed_instances_with_tasks_completed_by_me: ReportMetadata = { "columns": cls.builtin_column_options(), "filter_by": [ - {"field_name": "with_tasks_completed_by_me", "field_value": True}, - {"field_name": "process_status", "field_value": terminal_status_values}, + {"field_name": "with_tasks_completed_by_me", "field_value": True, "operator": "equals"}, + {"field_name": "process_status", "field_value": terminal_status_values, "operator": "equals"}, ], "order_by": ["-start_in_seconds", "-id"], } system_report_completed_instances: ReportMetadata = { "columns": cls.builtin_column_options(), "filter_by": [ - {"field_name": "process_status", "field_value": terminal_status_values}, + {"field_name": "process_status", "field_value": terminal_status_values, "operator": "equals"}, ], "order_by": ["-start_in_seconds", "-id"], } system_report_in_progress_instances_initiated_by_me: ReportMetadata = { "columns": [ - {"Header": "id", "accessor": "id"}, + {"Header": "id", "accessor": "id", "filterable": False}, { "Header": "process_model_display_name", "accessor": "process_model_display_name", + "filterable": False, }, - {"Header": "Task", "accessor": "task_title"}, - {"Header": "Waiting For", "accessor": "waiting_for"}, - {"Header": "Started", "accessor": "start_in_seconds"}, - {"Header": "Last Updated", "accessor": "task_updated_at_in_seconds"}, - {"Header": "status", "accessor": "status"}, + {"Header": "Task", "accessor": "task_title", "filterable": False}, + {"Header": "Waiting For", "accessor": "waiting_for", "filterable": False}, + {"Header": "Started", "accessor": "start_in_seconds", "filterable": False}, + {"Header": "Last Updated", "accessor": "task_updated_at_in_seconds", "filterable": False}, + {"Header": "status", "accessor": "status", "filterable": False}, ], "filter_by": [ - {"field_name": "initiated_by_me", "field_value": True}, - {"field_name": "process_status", "field_value": non_terminal_status_values}, + {"field_name": "initiated_by_me", "field_value": True, "operator": "equals"}, + {"field_name": "process_status", "field_value": non_terminal_status_values, "operator": "equals"}, { "field_name": "with_oldest_open_task", "field_value": True, + "operator": "equals", }, ], "order_by": ["-start_in_seconds", "-id"], } system_report_in_progress_instances_with_tasks_for_me: ReportMetadata = { "columns": [ - {"Header": "id", "accessor": "id"}, + {"Header": "id", "accessor": "id", "filterable": False}, { "Header": "process_model_display_name", "accessor": "process_model_display_name", + "filterable": False, }, - {"Header": "Task", "accessor": "task_title"}, - {"Header": "Started By", "accessor": "process_initiator_username"}, - {"Header": "Started", "accessor": "start_in_seconds"}, - {"Header": "Last Updated", "accessor": "task_updated_at_in_seconds"}, + {"Header": "Task", "accessor": "task_title", "filterable": False}, + {"Header": "Started By", "accessor": "process_initiator_username", "filterable": False}, + {"Header": "Started", "accessor": "start_in_seconds", "filterable": False}, + {"Header": "Last Updated", "accessor": "task_updated_at_in_seconds", "filterable": False}, ], "filter_by": [ - {"field_name": "with_tasks_i_can_complete", "field_value": True}, - {"field_name": "process_status", "field_value": active_status_values}, + {"field_name": "with_tasks_i_can_complete", "field_value": True, "operator": "equals"}, + {"field_name": "process_status", "field_value": active_status_values, "operator": "equals"}, { "field_name": "with_oldest_open_task", "field_value": True, + "operator": "equals", }, ], "order_by": ["-start_in_seconds", "-id"], } system_report_in_progress_instances_with_tasks: ReportMetadata = { "columns": [ - {"Header": "id", "accessor": "id"}, + {"Header": "id", "accessor": "id", "filterable": False}, { "Header": "process_model_display_name", "accessor": "process_model_display_name", + "filterable": False, }, - {"Header": "Task", "accessor": "task_title"}, - {"Header": "Started By", "accessor": "process_initiator_username"}, - {"Header": "Started", "accessor": "start_in_seconds"}, - {"Header": "Last Updated", "accessor": "task_updated_at_in_seconds"}, + {"Header": "Task", "accessor": "task_title", "filterable": False}, + {"Header": "Started By", "accessor": "process_initiator_username", "filterable": False}, + {"Header": "Started", "accessor": "start_in_seconds", "filterable": False}, + {"Header": "Last Updated", "accessor": "task_updated_at_in_seconds", "filterable": False}, ], "filter_by": [ - {"field_name": "process_status", "field_value": active_status_values}, + {"field_name": "process_status", "field_value": active_status_values, "operator": "equals"}, { "field_name": "with_oldest_open_task", "field_value": True, + "operator": "equals", }, ], "order_by": ["-start_in_seconds", "-id"], @@ -165,7 +172,8 @@ class ProcessInstanceReportService: } if metadata_key not in temp_system_metadata_map: return None - return temp_system_metadata_map[metadata_key] + return_value: ReportMetadata = temp_system_metadata_map[metadata_key] + return return_value @classmethod def compile_report(cls, report_metadata: ReportMetadata, user: UserModel) -> None: @@ -173,7 +181,9 @@ class ProcessInstanceReportService: old_filters = copy.deepcopy(report_metadata["filter_by"]) for filter in old_filters: if filter["field_name"] == "initiated_by_me": - compiled_filters.append({"field_name": "process_initiator_username", "field_value": user.username}) + compiled_filters.append( + {"field_name": "process_initiator_username", "field_value": user.username, "operator": "equals"} + ) else: compiled_filters.append(filter) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 20ef475a5..ce72759b1 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -792,7 +792,6 @@ class TestProcessApi(BaseTest): content_type="multipart/form-data", headers=self.logged_in_headers(with_super_admin_user), ) - assert response.status_code == 400 assert response.json is not None assert response.json["error_code"] == "no_file_given" @@ -1853,7 +1852,13 @@ class TestProcessApi(BaseTest): # Without filtering we should get all 5 instances report_metadata_body: ReportMetadata = { - "filter_by": [{"field_name": "process_model_identifier", "field_value": process_model_identifier}], + "filter_by": [ + { + "field_name": "process_model_identifier", + "field_value": process_model_identifier, + "operator": "equals", + } + ], "columns": [], "order_by": [], } @@ -1868,8 +1873,16 @@ class TestProcessApi(BaseTest): for i in range(5): report_metadata_body = { "filter_by": [ - {"field_name": "process_model_identifier", "field_value": process_model_identifier}, - {"field_name": "process_status", "field_value": ProcessInstanceStatus[statuses[i]].value}, + { + "field_name": "process_model_identifier", + "field_value": process_model_identifier, + "operator": "equals", + }, + { + "field_name": "process_status", + "field_value": ProcessInstanceStatus[statuses[i]].value, + "operator": "equals", + }, ], "columns": [], "order_by": [], @@ -1883,8 +1896,12 @@ class TestProcessApi(BaseTest): report_metadata_body = { "filter_by": [ - {"field_name": "process_model_identifier", "field_value": process_model_identifier}, - {"field_name": "process_status", "field_value": "not_started,complete"}, + { + "field_name": "process_model_identifier", + "field_value": process_model_identifier, + "operator": "equals", + }, + {"field_name": "process_status", "field_value": "not_started,complete", "operator": "equals"}, ], "columns": [], "order_by": [], @@ -1900,7 +1917,7 @@ class TestProcessApi(BaseTest): # filter by start/end seconds # start > 1000 - this should eliminate the first report_metadata_body = { - "filter_by": [{"field_name": "start_from", "field_value": 1001}], + "filter_by": [{"field_name": "start_from", "field_value": 1001, "operator": "equals"}], "columns": [], "order_by": [], } @@ -1920,8 +1937,8 @@ class TestProcessApi(BaseTest): # start > 2000, end < 5000 - this should eliminate the first 2 and the last report_metadata_body = { "filter_by": [ - {"field_name": "start_from", "field_value": 2001}, - {"field_name": "end_to", "field_value": 5999}, + {"field_name": "start_from", "field_value": 2001, "operator": "equals"}, + {"field_name": "end_to", "field_value": 5999, "operator": "equals"}, ], "columns": [], "order_by": [], @@ -1937,8 +1954,8 @@ class TestProcessApi(BaseTest): # start > 1000, start < 4000 - this should eliminate the first and the last 2 report_metadata_body = { "filter_by": [ - {"field_name": "start_from", "field_value": 1001}, - {"field_name": "start_to", "field_value": 3999}, + {"field_name": "start_from", "field_value": 1001, "operator": "equals"}, + {"field_name": "start_to", "field_value": 3999, "operator": "equals"}, ], "columns": [], "order_by": [], @@ -1954,8 +1971,8 @@ class TestProcessApi(BaseTest): # end > 2000, end < 6000 - this should eliminate the first and the last report_metadata_body = { "filter_by": [ - {"field_name": "end_from", "field_value": 2001}, - {"field_name": "end_to", "field_value": 5999}, + {"field_name": "end_from", "field_value": 2001, "operator": "equals"}, + {"field_name": "end_to", "field_value": 5999, "operator": "equals"}, ], "columns": [], "order_by": [],