From c2083103e45af10c6c4370769e4adf8ca3536ede Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 16 May 2023 17:24:22 -0400 Subject: [PATCH 01/60] added some framework stuff to run process model unit tests w/ burnettk --- .../services/file_system_service.py | 38 ++-- .../services/process_model_service.py | 31 +-- .../process_model_test_runner_service.py | 180 ++++++++++++++++++ .../services/spec_file_service.py | 22 +-- .../basic_script_task/basic_script_task.bpmn | 39 ++++ .../basic_script_task/process_model.json | 11 ++ .../test_basic_script_task.json | 5 + .../test_process_model_test_runner_service.py | 42 ++++ 8 files changed, 313 insertions(+), 55 deletions(-) create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json create mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py index 5cad69ad..b4b85a74 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py @@ -49,13 +49,12 @@ class FileSystemService: """Id_string_to_relative_path.""" return id_string.replace("/", os.sep) - @staticmethod - def process_group_path(name: str) -> str: - """Category_path.""" + @classmethod + def full_path_from_id(cls, id: str) -> str: return os.path.abspath( os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(name), + cls.root_path(), + cls.id_string_to_relative_path(id), ) ) @@ -65,36 +64,33 @@ class FileSystemService: return os.path.join(FileSystemService.root_path(), relative_path) @staticmethod - def process_model_relative_path(spec: ProcessModelInfo) -> str: + def process_model_relative_path(process_model: ProcessModelInfo) -> str: """Get the file path to a process model relative to SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR. If the full path is /path/to/process-group-a/group-b/process-model-a, it will return: process-group-a/group-b/process-model-a """ - workflow_path = FileSystemService.workflow_path(spec) + workflow_path = FileSystemService.process_model_full_path(process_model) return os.path.relpath(workflow_path, start=FileSystemService.root_path()) @staticmethod - def process_group_path_for_spec(spec: ProcessModelInfo) -> str: - """Category_path_for_spec.""" + def process_group_path_for_spec(process_model: ProcessModelInfo) -> str: # os.path.split apparently returns 2 element tulple like: (first/path, last_item) - process_group_id, _ = os.path.split(spec.id_for_file_path()) - return FileSystemService.process_group_path(process_group_id) + process_group_id, _ = os.path.split(process_model.id_for_file_path()) + return FileSystemService.full_path_from_id(process_group_id) + + @classmethod + def process_model_full_path(cls, process_model: ProcessModelInfo) -> str: + return cls.full_path_from_id(process_model.id) @staticmethod - def workflow_path(spec: ProcessModelInfo) -> str: - """Workflow_path.""" - process_model_path = os.path.join(FileSystemService.root_path(), spec.id_for_file_path()) - return process_model_path - - @staticmethod - def full_path_to_process_model_file(spec: ProcessModelInfo) -> str: + def full_path_to_process_model_file(process_model: ProcessModelInfo) -> str: """Full_path_to_process_model_file.""" - return os.path.join(FileSystemService.workflow_path(spec), spec.primary_file_name) # type: ignore + return os.path.join(FileSystemService.process_model_full_path(process_model), process_model.primary_file_name) # type: ignore - def next_display_order(self, spec: ProcessModelInfo) -> int: + def next_display_order(self, process_model: ProcessModelInfo) -> int: """Next_display_order.""" - path = self.process_group_path_for_spec(spec) + path = self.process_group_path_for_spec(process_model) if os.path.exists(path): return len(next(os.walk(path))[1]) else: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index 1cfc3339..fbf6587b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -60,12 +60,7 @@ class ProcessModelService(FileSystemService): def is_process_group_identifier(cls, process_group_identifier: str) -> bool: """Is_process_group_identifier.""" if os.path.exists(FileSystemService.root_path()): - process_group_path = os.path.abspath( - os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(process_group_identifier), - ) - ) + process_group_path = FileSystemService.full_path_from_id(process_group_identifier) return cls.is_process_group(process_group_path) return False @@ -82,12 +77,7 @@ class ProcessModelService(FileSystemService): def is_process_model_identifier(cls, process_model_identifier: str) -> bool: """Is_process_model_identifier.""" if os.path.exists(FileSystemService.root_path()): - process_model_path = os.path.abspath( - os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(process_model_identifier), - ) - ) + process_model_path = FileSystemService.full_path_from_id(process_model_identifier) return cls.is_process_model(process_model_path) return False @@ -149,13 +139,13 @@ class ProcessModelService(FileSystemService): f"We cannot delete the model `{process_model_id}`, there are existing instances that depend on it." ) process_model = self.get_process_model(process_model_id) - path = self.workflow_path(process_model) + path = self.process_model_full_path(process_model) shutil.rmtree(path) def process_model_move(self, original_process_model_id: str, new_location: str) -> ProcessModelInfo: """Process_model_move.""" process_model = self.get_process_model(original_process_model_id) - original_model_path = self.workflow_path(process_model) + original_model_path = self.process_model_full_path(process_model) _, model_id = os.path.split(original_model_path) new_relative_path = os.path.join(new_location, model_id) new_model_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_relative_path)) @@ -314,12 +304,7 @@ class ProcessModelService(FileSystemService): def get_process_group(cls, process_group_id: str, find_direct_nested_items: bool = True) -> ProcessGroup: """Look for a given process_group, and return it.""" if os.path.exists(FileSystemService.root_path()): - process_group_path = os.path.abspath( - os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(process_group_id), - ) - ) + process_group_path = FileSystemService.full_path_from_id(process_group_id) if cls.is_process_group(process_group_path): return cls.find_or_create_process_group( process_group_path, @@ -336,7 +321,7 @@ class ProcessModelService(FileSystemService): @classmethod def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: """Update_process_group.""" - cat_path = cls.process_group_path(process_group.id) + cat_path = cls.full_path_from_id(process_group.id) os.makedirs(cat_path, exist_ok=True) json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE) serialized_process_group = process_group.serialized @@ -348,7 +333,7 @@ class ProcessModelService(FileSystemService): def process_group_move(self, original_process_group_id: str, new_location: str) -> ProcessGroup: """Process_group_move.""" - original_group_path = self.process_group_path(original_process_group_id) + original_group_path = self.full_path_from_id(original_process_group_id) _, original_group_id = os.path.split(original_group_path) new_root = os.path.join(FileSystemService.root_path(), new_location) new_group_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_root, original_group_id)) @@ -370,7 +355,7 @@ class ProcessModelService(FileSystemService): def process_group_delete(self, process_group_id: str) -> None: """Delete_process_group.""" problem_models = [] - path = self.process_group_path(process_group_id) + path = self.full_path_from_id(process_group_id) if os.path.exists(path): nested_models = self.__get_all_nested_models(path) for process_model in nested_models: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py new file mode 100644 index 00000000..a6ae27fd --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -0,0 +1,180 @@ +from typing import List, Optional +from dataclasses import dataclass +import json +from SpiffWorkflow.task import Task as SpiffTask # type: ignore +from SpiffWorkflow.task import TaskState +from lxml import etree # type: ignore +from spiffworkflow_backend.services.spec_file_service import SpecFileService +from spiffworkflow_backend.services.custom_parser import MyCustomParser +from typing import Callable +import re +import glob +from spiffworkflow_backend.models.process_model import ProcessModelInfo +import os +from spiffworkflow_backend.services.file_system_service import FileSystemService +from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore + + +# workflow json for test case +# 1. default action is load xml from disk and use spiff like normal and get back workflow json +# 2. do stuff from disk cache + +# find all process models +# find all json test cases for each +# for each test case, fire up something like spiff +# for each task, if there is something special in the test case definition, do it (provide data for user task, mock service task, etc) +# when the thing is complete, check workflow data against expected data + + +class UnrunnableTestCaseError(Exception): + pass + + +@dataclass +class TestCaseResult: + passed: bool + test_case_name: str + error: Optional[str] = None + + +# input: +# json_file: +# { +# [TEST_CASE_NAME]: { +# "tasks": { +# [BPMN_TASK_IDENTIIFER]: [DATA] +# }, +# "expected_output_json": [DATA] +# } +# } +class ProcessModelTestRunner: + """Generic test runner code. May move into own library at some point. + + KEEP THIS GENERIC. do not add backend specific code here. + """ + def __init__( + self, + process_model_directory_path: str, + instantiate_executer_callback: Callable[[str], any], + execute_task_callback: Callable[[any, Optional[dict]], any], + get_next_task_callback: Callable[[any], any], + ) -> None: + self.process_model_directory_path = process_model_directory_path + self.test_mappings = self._discover_process_model_directories() + self.instantiate_executer_callback = instantiate_executer_callback + self.execute_task_callback = execute_task_callback + self.get_next_task_callback = get_next_task_callback + + self.test_case_results = [] + + def all_test_cases_passed(self) -> bool: + failed_tests = [t for t in self.test_case_results if t.passed is False] + return len(failed_tests) < 1 + + def run(self) -> None: + for json_test_case_file, bpmn_file in self.test_mappings.items(): + with open(json_test_case_file, 'rt') as f: + json_file_contents = json.loads(f.read()) + + for test_case_name, test_case_contents in json_file_contents.items(): + try: + self.run_test_case(bpmn_file, test_case_name, test_case_contents) + except Exception as ex: + self.test_case_results.append(TestCaseResult( + passed=False, + test_case_name=test_case_name, + error=f"Syntax error: {str(ex)}", + )) + + def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: + bpmn_process_instance = self.instantiate_executer_callback(bpmn_file) + next_task = self.get_next_task_callback(bpmn_process_instance) + while next_task is not None: + test_case_json = None + if 'tasks' in test_case_contents: + if next_task.task_spec.bpmn_id in test_case_contents['tasks']: + test_case_json = test_case_contents['tasks'][next_task.task_spec.bpmn_id] + + task_type = next_task.task_spec.__class__.__name__ + if task_type in ["ServiceTask", "UserTask"] and test_case_json is None: + raise UnrunnableTestCaseError( + f"Cannot run test case '{test_case_name}'. It requires task data for {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" + ) + self.execute_task_callback(next_task, test_case_json) + next_task = self.get_next_task_callback(bpmn_process_instance) + test_passed = test_case_contents['expected_output_json'] == bpmn_process_instance.data + self.test_case_results.append(TestCaseResult( + passed=test_passed, + test_case_name=test_case_name, + )) + + def _discover_process_model_directories( + self, + ) -> dict[str, str]: + test_mappings = {} + + json_test_file_glob = os.path.join(self.process_model_directory_path, "**", "test_*.json") + + for file in glob.glob(json_test_file_glob, recursive=True): + file_dir = os.path.dirname(file) + json_file_name = os.path.basename(file) + bpmn_file_name = re.sub(r'^test_(.*)\.json', r'\1.bpmn', json_file_name) + bpmn_file_path = os.path.join(file_dir, bpmn_file_name) + if os.path.isfile(bpmn_file_path): + test_mappings[file] = bpmn_file_path + return test_mappings + + +class BpmnFileMissingExecutableProcessError(Exception): + pass + + +class ProcessModelTestRunnerService: + def __init__( + self, + process_model_directory_path: str + ) -> None: + self.process_model_test_runner = ProcessModelTestRunner( + process_model_directory_path, + instantiate_executer_callback=self._instantiate_executer_callback, + execute_task_callback=self._execute_task_callback, + get_next_task_callback=self._get_next_task_callback, + ) + + def run(self) -> None: + self.process_model_test_runner.run() + + def _execute_task_callback(self, spiff_task: SpiffTask, _test_case_json: Optional[dict]) -> None: + spiff_task.run() + + def _get_next_task_callback(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + engine_steps = self._get_ready_engine_steps(bpmn_process_instance) + if len(engine_steps) > 0: + return engine_steps[0] + return None + + def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: + tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) + + if len(tasks) > 0: + tasks = [tasks[0]] + + return tasks + + def _instantiate_executer_callback(self, bpmn_file) -> BpmnWorkflow: + parser = MyCustomParser() + data = None + with open(bpmn_file, "rb") as f_handle: + data = f_handle.read() + bpmn: etree.Element = SpecFileService.get_etree_from_xml_bytes(data) + parser.add_bpmn_xml(bpmn, filename=os.path.basename(bpmn_file)) + sub_parsers = list(parser.process_parsers.values()) + executable_process = None + for sub_parser in sub_parsers: + if sub_parser.process_executable: + executable_process = sub_parser.bpmn_id + if executable_process is None: + raise BpmnFileMissingExecutableProcessError(f"Executable process cannot be found in {bpmn_file}. Test cannot run.") + bpmn_process_spec = parser.get_spec(executable_process) + bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) + return bpmn_process_instance diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index e8771738..9169c5d6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -221,37 +221,37 @@ class SpecFileService(FileSystemService): return spec_file_data @staticmethod - def full_file_path(spec: ProcessModelInfo, file_name: str) -> str: + def full_file_path(process_model: ProcessModelInfo, file_name: str) -> str: """File_path.""" - return os.path.abspath(os.path.join(SpecFileService.workflow_path(spec), file_name)) + return os.path.abspath(os.path.join(SpecFileService.process_model_full_path(process_model), file_name)) @staticmethod - def last_modified(spec: ProcessModelInfo, file_name: str) -> datetime: + def last_modified(process_model: ProcessModelInfo, file_name: str) -> datetime: """Last_modified.""" - full_file_path = SpecFileService.full_file_path(spec, file_name) + full_file_path = SpecFileService.full_file_path(process_model, file_name) return FileSystemService._last_modified(full_file_path) @staticmethod - def timestamp(spec: ProcessModelInfo, file_name: str) -> float: + def timestamp(process_model: ProcessModelInfo, file_name: str) -> float: """Timestamp.""" - full_file_path = SpecFileService.full_file_path(spec, file_name) + full_file_path = SpecFileService.full_file_path(process_model, file_name) return FileSystemService._timestamp(full_file_path) @staticmethod - def delete_file(spec: ProcessModelInfo, file_name: str) -> None: + def delete_file(process_model: ProcessModelInfo, file_name: str) -> None: """Delete_file.""" - # Fixme: Remember to remove the lookup files when the spec file is removed. + # Fixme: Remember to remove the lookup files when the process_model file is removed. # lookup_files = session.query(LookupFileModel).filter_by(file_model_id=file_id).all() # for lf in lookup_files: # session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete() # session.query(LookupFileModel).filter_by(id=lf.id).delete() - full_file_path = SpecFileService.full_file_path(spec, file_name) + full_file_path = SpecFileService.full_file_path(process_model, file_name) os.remove(full_file_path) @staticmethod - def delete_all_files(spec: ProcessModelInfo) -> None: + def delete_all_files(process_model: ProcessModelInfo) -> None: """Delete_all_files.""" - dir_path = SpecFileService.workflow_path(spec) + dir_path = SpecFileService.process_model_full_path(process_model) if os.path.exists(dir_path): shutil.rmtree(dir_path) diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn new file mode 100644 index 00000000..3a5302e6 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn @@ -0,0 +1,39 @@ + + + + + Flow_0qfycuk + + + + Flow_1auiekw + + + + Flow_0qfycuk + Flow_1auiekw + a = 1 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json new file mode 100644 index 00000000..03d72515 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Script Task", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "Script.bpmn", + "primary_process_id": "Process_Script_Task" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json new file mode 100644 index 00000000..8eb2df13 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json @@ -0,0 +1,5 @@ +{ + "test_case_one": { + "expected_output_json": { "a": 1 } + } +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py new file mode 100644 index 00000000..b0932235 --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -0,0 +1,42 @@ +from flask import Flask +import pytest +import os +from flask import current_app +from spiffworkflow_backend.models.process_model import ProcessModelInfo +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService +from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec +from pytest_mock import MockerFixture + +from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel +from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel +from spiffworkflow_backend.models.task import TaskModel # noqa: F401 +from spiffworkflow_backend.models.task_definition import TaskDefinitionModel +from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor +from spiffworkflow_backend.services.task_service import TaskService + + +class TestProcessModelTestRunnerService(BaseTest): + def test_can_test_a_simple_process_model( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: any, + ) -> None: + test_runner_service = ProcessModelTestRunnerService(os.path.join(FileSystemService.root_path(), 'basic_script_task')) + test_runner_service.run() + assert test_runner_service.process_model_test_runner.all_test_cases_passed() + + @pytest.fixture() + def with_mocked_root_path(self, mocker: MockerFixture) -> None: + path = os.path.join( + current_app.instance_path, + "..", + "..", + "tests", + "data", + "bpmn_unit_test_process_models", + ) + mocker.patch.object(FileSystemService, attribute='root_path', return_value=path) From 2f98891489a55b8db7edb54ef07dcac6c83c2f78 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 16 May 2023 17:32:53 -0400 Subject: [PATCH 02/60] added test for failing test and multiple at once w/ burnettk --- .../process_model_test_runner_service.py | 6 +++ .../basic_failing_script_task.bpmn | 41 +++++++++++++++++++ .../process_model.json | 11 +++++ .../test_basic_failing_script_task.json | 3 ++ .../test_process_model_test_runner_service.py | 10 +++++ 5 files changed, 71 insertions(+) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index a6ae27fd..a615dc5a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -30,6 +30,10 @@ class UnrunnableTestCaseError(Exception): pass +class MissingBpmnFileForTestCaseError(Exception): + pass + + @dataclass class TestCaseResult: passed: bool @@ -122,6 +126,8 @@ class ProcessModelTestRunner: bpmn_file_path = os.path.join(file_dir, bpmn_file_name) if os.path.isfile(bpmn_file_path): test_mappings[file] = bpmn_file_path + else: + raise MissingBpmnFileForTestCaseError(f"Cannot find a matching bpmn file for test case json file: '{file}'") return test_mappings diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn new file mode 100644 index 00000000..4b046335 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn @@ -0,0 +1,41 @@ + + + + + Flow_1xkc1ru + + + + Flow_0tkkq9s + + + + Flow_1xkc1ru + Flow_0tkkq9s + a = 1 +b = a + 'two' + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json new file mode 100644 index 00000000..26f25510 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "Process that raises an exception", + "display_name": "Failing Process", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "failing_task.bpmn", + "primary_process_id": "Process_FailingProcess" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json new file mode 100644 index 00000000..55360600 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json @@ -0,0 +1,3 @@ +{ + "test_case_two": {} +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py index b0932235..d202a35c 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -29,6 +29,16 @@ class TestProcessModelTestRunnerService(BaseTest): test_runner_service.run() assert test_runner_service.process_model_test_runner.all_test_cases_passed() + def test_can_test_multiple_process_models( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: any, + ) -> None: + test_runner_service = ProcessModelTestRunnerService(FileSystemService.root_path()) + test_runner_service.run() + assert test_runner_service.process_model_test_runner.all_test_cases_passed() is False + @pytest.fixture() def with_mocked_root_path(self, mocker: MockerFixture) -> None: path = os.path.join( From 3d35dc6213e2b7008d811abb89ffcc0cc72adca6 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 10:01:11 -0400 Subject: [PATCH 03/60] pyl --- .../spiffworkflow_backend/config/__init__.py | 8 +- .../services/file_system_service.py | 4 +- .../process_model_test_runner_service.py | 89 ++++++++++--------- .../process_model.json | 2 +- .../test_process_model_test_runner_service.py | 34 ++++--- 5 files changed, 73 insertions(+), 64 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py index 55c95897..22479110 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py @@ -40,7 +40,8 @@ def setup_database_configs(app: Flask) -> None: if pool_size is not None: pool_size = int(pool_size) else: - # this one doesn't come from app config and isn't documented in default.py because we don't want to give people the impression + # this one doesn't come from app config and isn't documented in default.py + # because we don't want to give people the impression # that setting it in flask python configs will work. on the contrary, it's used by a bash # script that starts the backend, so it can only be set in the environment. threads_per_worker_config = os.environ.get("SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER") @@ -50,8 +51,9 @@ def setup_database_configs(app: Flask) -> None: # this is a sqlalchemy default, if we don't have any better ideas pool_size = 5 - app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {} - app.config['SQLALCHEMY_ENGINE_OPTIONS']['pool_size'] = pool_size + app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {} + app.config["SQLALCHEMY_ENGINE_OPTIONS"]["pool_size"] = pool_size + def load_config_file(app: Flask, env_config_module: str) -> None: """Load_config_file.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py index b4b85a74..be38f886 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py @@ -86,7 +86,9 @@ class FileSystemService: @staticmethod def full_path_to_process_model_file(process_model: ProcessModelInfo) -> str: """Full_path_to_process_model_file.""" - return os.path.join(FileSystemService.process_model_full_path(process_model), process_model.primary_file_name) # type: ignore + return os.path.join( + FileSystemService.process_model_full_path(process_model), process_model.primary_file_name # type: ignore + ) def next_display_order(self, process_model: ProcessModelInfo) -> int: """Next_display_order.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index a615dc5a..df6e76ef 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -1,18 +1,19 @@ -from typing import List, Optional -from dataclasses import dataclass -import json -from SpiffWorkflow.task import Task as SpiffTask # type: ignore -from SpiffWorkflow.task import TaskState -from lxml import etree # type: ignore -from spiffworkflow_backend.services.spec_file_service import SpecFileService -from spiffworkflow_backend.services.custom_parser import MyCustomParser -from typing import Callable -import re import glob -from spiffworkflow_backend.models.process_model import ProcessModelInfo +import json import os -from spiffworkflow_backend.services.file_system_service import FileSystemService +import re +from dataclasses import dataclass +from typing import Any +from typing import Callable +from typing import Optional + +from lxml import etree # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore +from SpiffWorkflow.task import Task as SpiffTask # type: ignore +from SpiffWorkflow.task import TaskState + +from spiffworkflow_backend.services.custom_parser import MyCustomParser +from spiffworkflow_backend.services.spec_file_service import SpecFileService # workflow json for test case @@ -22,7 +23,8 @@ from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore # find all process models # find all json test cases for each # for each test case, fire up something like spiff -# for each task, if there is something special in the test case definition, do it (provide data for user task, mock service task, etc) +# for each task, if there is something special in the test case definition, +# do it (provide data for user task, mock service task, etc) # when the thing is complete, check workflow data against expected data @@ -56,12 +58,13 @@ class ProcessModelTestRunner: KEEP THIS GENERIC. do not add backend specific code here. """ + def __init__( self, process_model_directory_path: str, - instantiate_executer_callback: Callable[[str], any], - execute_task_callback: Callable[[any, Optional[dict]], any], - get_next_task_callback: Callable[[any], any], + instantiate_executer_callback: Callable[[str], Any], + execute_task_callback: Callable[[Any, Optional[dict]], Any], + get_next_task_callback: Callable[[Any], Any], ) -> None: self.process_model_directory_path = process_model_directory_path self.test_mappings = self._discover_process_model_directories() @@ -69,7 +72,7 @@ class ProcessModelTestRunner: self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback - self.test_case_results = [] + self.test_case_results: list[TestCaseResult] = [] def all_test_cases_passed(self) -> bool: failed_tests = [t for t in self.test_case_results if t.passed is False] @@ -77,40 +80,45 @@ class ProcessModelTestRunner: def run(self) -> None: for json_test_case_file, bpmn_file in self.test_mappings.items(): - with open(json_test_case_file, 'rt') as f: + with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) for test_case_name, test_case_contents in json_file_contents.items(): try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - self.test_case_results.append(TestCaseResult( - passed=False, - test_case_name=test_case_name, - error=f"Syntax error: {str(ex)}", - )) + self.test_case_results.append( + TestCaseResult( + passed=False, + test_case_name=test_case_name, + error=f"Syntax error: {str(ex)}", + ) + ) def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: bpmn_process_instance = self.instantiate_executer_callback(bpmn_file) next_task = self.get_next_task_callback(bpmn_process_instance) while next_task is not None: test_case_json = None - if 'tasks' in test_case_contents: - if next_task.task_spec.bpmn_id in test_case_contents['tasks']: - test_case_json = test_case_contents['tasks'][next_task.task_spec.bpmn_id] + if "tasks" in test_case_contents: + if next_task.task_spec.bpmn_id in test_case_contents["tasks"]: + test_case_json = test_case_contents["tasks"][next_task.task_spec.bpmn_id] task_type = next_task.task_spec.__class__.__name__ if task_type in ["ServiceTask", "UserTask"] and test_case_json is None: raise UnrunnableTestCaseError( - f"Cannot run test case '{test_case_name}'. It requires task data for {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" + f"Cannot run test case '{test_case_name}'. It requires task data for" + f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) self.execute_task_callback(next_task, test_case_json) next_task = self.get_next_task_callback(bpmn_process_instance) - test_passed = test_case_contents['expected_output_json'] == bpmn_process_instance.data - self.test_case_results.append(TestCaseResult( - passed=test_passed, - test_case_name=test_case_name, - )) + test_passed = test_case_contents["expected_output_json"] == bpmn_process_instance.data + self.test_case_results.append( + TestCaseResult( + passed=test_passed, + test_case_name=test_case_name, + ) + ) def _discover_process_model_directories( self, @@ -122,12 +130,14 @@ class ProcessModelTestRunner: for file in glob.glob(json_test_file_glob, recursive=True): file_dir = os.path.dirname(file) json_file_name = os.path.basename(file) - bpmn_file_name = re.sub(r'^test_(.*)\.json', r'\1.bpmn', json_file_name) + bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) bpmn_file_path = os.path.join(file_dir, bpmn_file_name) if os.path.isfile(bpmn_file_path): test_mappings[file] = bpmn_file_path else: - raise MissingBpmnFileForTestCaseError(f"Cannot find a matching bpmn file for test case json file: '{file}'") + raise MissingBpmnFileForTestCaseError( + f"Cannot find a matching bpmn file for test case json file: '{file}'" + ) return test_mappings @@ -136,10 +146,7 @@ class BpmnFileMissingExecutableProcessError(Exception): class ProcessModelTestRunnerService: - def __init__( - self, - process_model_directory_path: str - ) -> None: + def __init__(self, process_model_directory_path: str) -> None: self.process_model_test_runner = ProcessModelTestRunner( process_model_directory_path, instantiate_executer_callback=self._instantiate_executer_callback, @@ -167,7 +174,7 @@ class ProcessModelTestRunnerService: return tasks - def _instantiate_executer_callback(self, bpmn_file) -> BpmnWorkflow: + def _instantiate_executer_callback(self, bpmn_file: str) -> BpmnWorkflow: parser = MyCustomParser() data = None with open(bpmn_file, "rb") as f_handle: @@ -180,7 +187,9 @@ class ProcessModelTestRunnerService: if sub_parser.process_executable: executable_process = sub_parser.bpmn_id if executable_process is None: - raise BpmnFileMissingExecutableProcessError(f"Executable process cannot be found in {bpmn_file}. Test cannot run.") + raise BpmnFileMissingExecutableProcessError( + f"Executable process cannot be found in {bpmn_file}. Test cannot run." + ) bpmn_process_spec = parser.get_spec(executable_process) bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) return bpmn_process_instance diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json index 26f25510..23cc190b 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json @@ -8,4 +8,4 @@ "metadata_extraction_paths": null, "primary_file_name": "failing_task.bpmn", "primary_process_id": "Process_FailingProcess" -} \ No newline at end of file +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py index d202a35c..a993c945 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -1,21 +1,15 @@ -from flask import Flask -import pytest import os -from flask import current_app -from spiffworkflow_backend.models.process_model import ProcessModelInfo -from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_service import ProcessModelService -from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService -from tests.spiffworkflow_backend.helpers.base_test import BaseTest -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from pytest_mock import MockerFixture +from typing import Any + +import pytest +from flask import current_app +from flask import Flask +from pytest_mock import MockerFixture +from tests.spiffworkflow_backend.helpers.base_test import BaseTest -from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel -from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.task import TaskModel # noqa: F401 -from spiffworkflow_backend.models.task_definition import TaskDefinitionModel -from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor -from spiffworkflow_backend.services.task_service import TaskService +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunnerService class TestProcessModelTestRunnerService(BaseTest): @@ -23,9 +17,11 @@ class TestProcessModelTestRunnerService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: any, + with_mocked_root_path: Any, ) -> None: - test_runner_service = ProcessModelTestRunnerService(os.path.join(FileSystemService.root_path(), 'basic_script_task')) + test_runner_service = ProcessModelTestRunnerService( + os.path.join(FileSystemService.root_path(), "basic_script_task") + ) test_runner_service.run() assert test_runner_service.process_model_test_runner.all_test_cases_passed() @@ -33,7 +29,7 @@ class TestProcessModelTestRunnerService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: any, + with_mocked_root_path: Any, ) -> None: test_runner_service = ProcessModelTestRunnerService(FileSystemService.root_path()) test_runner_service.run() @@ -49,4 +45,4 @@ class TestProcessModelTestRunnerService(BaseTest): "data", "bpmn_unit_test_process_models", ) - mocker.patch.object(FileSystemService, attribute='root_path', return_value=path) + mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) From 1cd2a794ebb6c46a848358643fbaae9808bb7b37 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 10:16:09 -0400 Subject: [PATCH 04/60] no reason to instantiate a ProcessModelService --- spiffworkflow-backend/conftest.py | 5 +- .../routes/process_groups_controller.py | 6 +-- .../routes/process_models_controller.py | 6 +-- .../services/process_instance_processor.py | 3 +- .../services/process_instance_service.py | 3 +- .../services/process_model_service.py | 53 +++++++++---------- .../helpers/test_data.py | 6 +-- 7 files changed, 37 insertions(+), 45 deletions(-) diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index 45e9fd54..c18c4936 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -55,9 +55,8 @@ def with_db_and_bpmn_file_cleanup() -> None: try: yield finally: - process_model_service = ProcessModelService() - if os.path.exists(process_model_service.root_path()): - shutil.rmtree(process_model_service.root_path()) + if os.path.exists(ProcessModelService.root_path()): + shutil.rmtree(ProcessModelService.root_path()) @pytest.fixture() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py index 6d1a479a..1901300d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py @@ -53,7 +53,7 @@ def process_group_delete(modified_process_group_id: str) -> flask.wrappers.Respo process_group_id = _un_modify_modified_process_model_id(modified_process_group_id) try: - ProcessModelService().process_group_delete(process_group_id) + ProcessModelService.process_group_delete(process_group_id) except ProcessModelWithInstancesNotDeletableError as exception: raise ApiError( error_code="existing_instances", @@ -88,7 +88,7 @@ def process_group_list( process_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100 ) -> flask.wrappers.Response: process_groups = ProcessModelService.get_process_groups_for_api(process_group_identifier) - batch = ProcessModelService().get_batch(items=process_groups, page=page, per_page=per_page) + batch = ProcessModelService.get_batch(items=process_groups, page=page, per_page=per_page) pages = len(process_groups) // per_page remainder = len(process_groups) % per_page if remainder > 0: @@ -128,7 +128,7 @@ def process_group_show( def process_group_move(modified_process_group_identifier: str, new_location: str) -> flask.wrappers.Response: """Process_group_move.""" original_process_group_id = _un_modify_modified_process_model_id(modified_process_group_identifier) - new_process_group = ProcessModelService().process_group_move(original_process_group_id, new_location) + new_process_group = ProcessModelService.process_group_move(original_process_group_id, new_location) _commit_and_push_to_git( f"User: {g.user.username} moved process group {original_process_group_id} to {new_process_group.id}" ) 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 eb7f2f9b..00d82639 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -104,7 +104,7 @@ def process_model_delete( """Process_model_delete.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") try: - ProcessModelService().process_model_delete(process_model_identifier) + ProcessModelService.process_model_delete(process_model_identifier) except ProcessModelWithInstancesNotDeletableError as exception: raise ApiError( error_code="existing_instances", @@ -182,7 +182,7 @@ def process_model_show(modified_process_model_identifier: str, include_file_refe def process_model_move(modified_process_model_identifier: str, new_location: str) -> flask.wrappers.Response: """Process_model_move.""" original_process_model_id = _un_modify_modified_process_model_id(modified_process_model_identifier) - new_process_model = ProcessModelService().process_model_move(original_process_model_id, new_location) + new_process_model = ProcessModelService.process_model_move(original_process_model_id, new_location) _commit_and_push_to_git( f"User: {g.user.username} moved process model {original_process_model_id} to {new_process_model.id}" ) @@ -219,7 +219,7 @@ def process_model_list( recursive=recursive, filter_runnable_by_user=filter_runnable_by_user, ) - process_models_to_return = ProcessModelService().get_batch(process_models, page=page, per_page=per_page) + process_models_to_return = ProcessModelService.get_batch(process_models, page=page, per_page=per_page) if include_parent_groups: process_group_cache = IdToProcessGroupMapping({}) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index b64cedfd..1c740c2d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -420,7 +420,6 @@ class ProcessInstanceProcessor: ) self.process_instance_model = process_instance_model - self.process_model_service = ProcessModelService() bpmn_process_spec = None self.full_bpmn_process_dict: dict = {} @@ -1018,7 +1017,7 @@ class ProcessInstanceProcessor: ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks() process_model_display_name = "" - process_model_info = self.process_model_service.get_process_model( + process_model_info = ProcessModelService.get_process_model( self.process_instance_model.process_model_identifier ) if process_model_info is not None: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index 84c62f12..0bb1c689 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -192,8 +192,7 @@ class ProcessInstanceService: """ # navigation = processor.bpmn_process_instance.get_deep_nav_list() # ProcessInstanceService.update_navigation(navigation, processor) - process_model_service = ProcessModelService() - process_model_service.get_process_model(processor.process_model_identifier) + ProcessModelService.get_process_model(processor.process_model_identifier) process_instance_api = ProcessInstanceApi( id=processor.get_process_instance_id(), status=processor.get_status(), diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index fbf6587b..dd771089 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -94,7 +94,6 @@ class ProcessModelService(FileSystemService): page: int = 1, per_page: int = 10, ) -> list[T]: - """Get_batch.""" start = (page - 1) * per_page end = start + per_page return items[start:end] @@ -129,8 +128,8 @@ class ProcessModelService(FileSystemService): cls.write_json_file(json_path, json_data) process_model.id = process_model_id - def process_model_delete(self, process_model_id: str) -> None: - """Delete Procecss Model.""" + @classmethod + def process_model_delete(cls, process_model_id: str) -> None: instances = ProcessInstanceModel.query.filter( ProcessInstanceModel.process_model_identifier == process_model_id ).all() @@ -138,19 +137,19 @@ class ProcessModelService(FileSystemService): raise ProcessModelWithInstancesNotDeletableError( f"We cannot delete the model `{process_model_id}`, there are existing instances that depend on it." ) - process_model = self.get_process_model(process_model_id) - path = self.process_model_full_path(process_model) + process_model = cls.get_process_model(process_model_id) + path = cls.process_model_full_path(process_model) shutil.rmtree(path) - def process_model_move(self, original_process_model_id: str, new_location: str) -> ProcessModelInfo: - """Process_model_move.""" - process_model = self.get_process_model(original_process_model_id) - original_model_path = self.process_model_full_path(process_model) + @classmethod + def process_model_move(cls, original_process_model_id: str, new_location: str) -> ProcessModelInfo: + process_model = cls.get_process_model(original_process_model_id) + original_model_path = cls.process_model_full_path(process_model) _, model_id = os.path.split(original_model_path) new_relative_path = os.path.join(new_location, model_id) new_model_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_relative_path)) shutil.move(original_model_path, new_model_path) - new_process_model = self.get_process_model(new_relative_path) + new_process_model = cls.get_process_model(new_relative_path) return new_process_model @classmethod @@ -315,12 +314,10 @@ class ProcessModelService(FileSystemService): @classmethod def add_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: - """Add_process_group.""" return cls.update_process_group(process_group) @classmethod def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: - """Update_process_group.""" cat_path = cls.full_path_from_id(process_group.id) os.makedirs(cat_path, exist_ok=True) json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE) @@ -331,33 +328,33 @@ class ProcessModelService(FileSystemService): cls.write_json_file(json_path, serialized_process_group) return process_group - def process_group_move(self, original_process_group_id: str, new_location: str) -> ProcessGroup: - """Process_group_move.""" - original_group_path = self.full_path_from_id(original_process_group_id) + @classmethod + def process_group_move(cls, original_process_group_id: str, new_location: str) -> ProcessGroup: + original_group_path = cls.full_path_from_id(original_process_group_id) _, original_group_id = os.path.split(original_group_path) new_root = os.path.join(FileSystemService.root_path(), new_location) new_group_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_root, original_group_id)) destination = shutil.move(original_group_path, new_group_path) - new_process_group = self.get_process_group(destination) + new_process_group = cls.get_process_group(destination) return new_process_group - def __get_all_nested_models(self, group_path: str) -> list: - """__get_all_nested_models.""" + @classmethod + def __get_all_nested_models(cls, group_path: str) -> list: all_nested_models = [] for _root, dirs, _files in os.walk(group_path): for dir in dirs: model_dir = os.path.join(group_path, dir) if ProcessModelService.is_process_model(model_dir): - process_model = self.get_process_model(model_dir) + process_model = cls.get_process_model(model_dir) all_nested_models.append(process_model) return all_nested_models - def process_group_delete(self, process_group_id: str) -> None: - """Delete_process_group.""" + @classmethod + def process_group_delete(cls, process_group_id: str) -> None: problem_models = [] - path = self.full_path_from_id(process_group_id) + path = cls.full_path_from_id(process_group_id) if os.path.exists(path): - nested_models = self.__get_all_nested_models(path) + nested_models = cls.__get_all_nested_models(path) for process_model in nested_models: instances = ProcessInstanceModel.query.filter( ProcessInstanceModel.process_model_identifier == process_model.id @@ -371,15 +368,15 @@ class ProcessModelService(FileSystemService): f" {problem_models}" ) shutil.rmtree(path) - self.cleanup_process_group_display_order() + cls._cleanup_process_group_display_order() - def cleanup_process_group_display_order(self) -> List[Any]: - """Cleanup_process_group_display_order.""" - process_groups = self.get_process_groups() # Returns an ordered list + @classmethod + def _cleanup_process_group_display_order(cls) -> List[Any]: + process_groups = cls.get_process_groups() # Returns an ordered list index = 0 for process_group in process_groups: process_group.display_order = index - self.update_process_group(process_group) + cls.update_process_group(process_group) index += 1 return process_groups diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py index 5a7a969e..7af2cfdc 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py @@ -12,12 +12,10 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer def assure_process_group_exists(process_group_id: Optional[str] = None) -> ProcessGroup: - """Assure_process_group_exists.""" process_group = None - process_model_service = ProcessModelService() if process_group_id is not None: try: - process_group = process_model_service.get_process_group(process_group_id) + process_group = ProcessModelService.get_process_group(process_group_id) except ProcessEntityNotFoundError: process_group = None @@ -31,7 +29,7 @@ def assure_process_group_exists(process_group_id: Optional[str] = None) -> Proce admin=False, display_order=0, ) - process_model_service.add_process_group(process_group) + ProcessModelService.add_process_group(process_group) return process_group From acaf3a3c24a714c80260bec475a5981397ed1b98 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 16:35:04 -0400 Subject: [PATCH 05/60] support call activities in process model test runner w/ burnettk --- .../process_model_test_runner_service.py | 188 ++++++++++++++---- .../basic_call_activity.bpmn | 40 ++++ .../basic_call_activity/process_model.json | 9 + .../test_basic_call_activity.json | 5 + .../basic_manual_task/basic_manual_task.bpmn | 39 ++++ .../basic_manual_task/process_model.json | 9 + .../test_basic_manual_task.json | 10 + .../test_process_model_test_runner_service.py | 17 +- 8 files changed, 280 insertions(+), 37 deletions(-) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index df6e76ef..4093e2f6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -39,16 +39,26 @@ class MissingBpmnFileForTestCaseError(Exception): @dataclass class TestCaseResult: passed: bool + bpmn_file: str test_case_name: str error: Optional[str] = None +DEFAULT_NSMAP = { + 'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL', + 'bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI', + 'dc': 'http://www.omg.org/spec/DD/20100524/DC', +} + + # input: # json_file: # { # [TEST_CASE_NAME]: { # "tasks": { -# [BPMN_TASK_IDENTIIFER]: [DATA] +# [BPMN_TASK_IDENTIIFER]: { +# "data": [DATA] +# } # }, # "expected_output_json": [DATA] # } @@ -62,17 +72,23 @@ class ProcessModelTestRunner: def __init__( self, process_model_directory_path: str, - instantiate_executer_callback: Callable[[str], Any], - execute_task_callback: Callable[[Any, Optional[dict]], Any], - get_next_task_callback: Callable[[Any], Any], + process_model_directory_for_test_discovery: Optional[str] = None, + instantiate_executer_callback: Optional[Callable[[str], Any]] = None, + execute_task_callback: Optional[Callable[[Any, Optional[dict]], Any]] = None, + get_next_task_callback: Optional[Callable[[Any], Any]] = None, ) -> None: self.process_model_directory_path = process_model_directory_path - self.test_mappings = self._discover_process_model_directories() + self.process_model_directory_for_test_discovery = process_model_directory_for_test_discovery or process_model_directory_path self.instantiate_executer_callback = instantiate_executer_callback self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback self.test_case_results: list[TestCaseResult] = [] + self.bpmn_processes_to_file_mappings: dict[str, str] = {} + self.bpmn_files_to_called_element_mappings: dict[str, list[str]] = {} + + self.test_mappings = self._discover_process_model_test_cases() + self._discover_process_model_processes() def all_test_cases_passed(self) -> bool: failed_tests = [t for t in self.test_case_results if t.passed is False] @@ -87,59 +103,161 @@ class ProcessModelTestRunner: try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - self.test_case_results.append( - TestCaseResult( - passed=False, - test_case_name=test_case_name, - error=f"Syntax error: {str(ex)}", - ) - ) + self._add_test_result(False, bpmn_file, test_case_name, f"Syntax error: {str(ex)}") def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: - bpmn_process_instance = self.instantiate_executer_callback(bpmn_file) - next_task = self.get_next_task_callback(bpmn_process_instance) + bpmn_process_instance = self._instantiate_executer(bpmn_file) + next_task = self._get_next_task(bpmn_process_instance) while next_task is not None: - test_case_json = None + test_case_task_properties = None if "tasks" in test_case_contents: if next_task.task_spec.bpmn_id in test_case_contents["tasks"]: - test_case_json = test_case_contents["tasks"][next_task.task_spec.bpmn_id] + test_case_task_properties = test_case_contents["tasks"][next_task.task_spec.bpmn_id] task_type = next_task.task_spec.__class__.__name__ - if task_type in ["ServiceTask", "UserTask"] and test_case_json is None: + if task_type in ["ServiceTask", "UserTask", "CallActivity"] and test_case_task_properties is None: raise UnrunnableTestCaseError( f"Cannot run test case '{test_case_name}'. It requires task data for" f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) - self.execute_task_callback(next_task, test_case_json) - next_task = self.get_next_task_callback(bpmn_process_instance) + self._execute_task(next_task, test_case_task_properties) + next_task = self._get_next_task(bpmn_process_instance) test_passed = test_case_contents["expected_output_json"] == bpmn_process_instance.data - self.test_case_results.append( - TestCaseResult( - passed=test_passed, - test_case_name=test_case_name, + error_message = None + if test_passed is False: + error_message = ( + f"Expected output did not match actual output:" + f"\nexpected: {test_case_contents['expected_output_json']}" + f"\nactual: {bpmn_process_instance.data}" ) - ) + self._add_test_result(test_passed, bpmn_file, test_case_name, error_message) - def _discover_process_model_directories( + def _discover_process_model_test_cases( self, ) -> dict[str, str]: test_mappings = {} - json_test_file_glob = os.path.join(self.process_model_directory_path, "**", "test_*.json") + json_test_file_glob = os.path.join(self.process_model_directory_for_test_discovery, "**", "test_*.json") for file in glob.glob(json_test_file_glob, recursive=True): - file_dir = os.path.dirname(file) - json_file_name = os.path.basename(file) + file_norm = os.path.normpath(file) + file_dir = os.path.dirname(file_norm) + json_file_name = os.path.basename(file_norm) bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) bpmn_file_path = os.path.join(file_dir, bpmn_file_name) if os.path.isfile(bpmn_file_path): - test_mappings[file] = bpmn_file_path + test_mappings[file_norm] = bpmn_file_path else: raise MissingBpmnFileForTestCaseError( - f"Cannot find a matching bpmn file for test case json file: '{file}'" + f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" ) return test_mappings + def _discover_process_model_processes( + self, + ) -> None: + process_model_bpmn_file_glob = os.path.join(self.process_model_directory_path, "**", "*.bpmn") + + for file in glob.glob(process_model_bpmn_file_glob, recursive=True): + file_norm = os.path.normpath(file) + if file_norm not in self.bpmn_files_to_called_element_mappings: + self.bpmn_files_to_called_element_mappings[file_norm] = [] + with open(file_norm, 'rb') as f: + file_contents = f.read() + etree_xml_parser = etree.XMLParser(resolve_entities=False) + root = etree.fromstring(file_contents, parser=etree_xml_parser) + call_activities = root.findall('.//bpmn:callActivity', namespaces=DEFAULT_NSMAP) + for call_activity in call_activities: + called_element = call_activity.attrib['calledElement'] + self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) + bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) + bpmn_process_identifier = bpmn_process_element.attrib['id'] + self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm + + def _execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: + if self.execute_task_callback: + self.execute_task_callback(spiff_task, test_case_task_properties) + self._default_execute_task(spiff_task, test_case_task_properties) + + def _get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + if self.get_next_task_callback: + return self.get_next_task_callback(bpmn_process_instance) + return self._default_get_next_task(bpmn_process_instance) + + def _instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: + if self.instantiate_executer_callback: + return self.instantiate_executer_callback(bpmn_file) + return self._default_instantiate_executer(bpmn_file) + + def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: + tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) + if len(tasks) > 0: + tasks = [tasks[0]] + + return tasks + + def _default_get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + engine_steps = self._get_ready_engine_steps(bpmn_process_instance) + if len(engine_steps) > 0: + return engine_steps[0] + return None + + def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: + if spiff_task.task_spec.manual: + if test_case_task_properties and 'data' in test_case_task_properties: + spiff_task.update_data(test_case_task_properties['data']) + spiff_task.complete() + else: + spiff_task.run() + + def _find_related_bpmn_files(self, bpmn_file: str) -> list[str]: + related_bpmn_files = [] + if bpmn_file in self.bpmn_files_to_called_element_mappings: + for bpmn_process_identifier in self.bpmn_files_to_called_element_mappings[bpmn_file]: + if bpmn_process_identifier in self.bpmn_processes_to_file_mappings: + new_file = self.bpmn_processes_to_file_mappings[bpmn_process_identifier] + related_bpmn_files.append(new_file) + related_bpmn_files.extend(self._find_related_bpmn_files(new_file)) + return related_bpmn_files + + def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree.Element: + data = None + with open(bpmn_file, "rb") as f_handle: + data = f_handle.read() + etree_xml_parser = etree.XMLParser(resolve_entities=False) + return etree.fromstring(data, parser=etree_xml_parser) + + def _default_instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: + parser = MyCustomParser() + bpmn_file_etree = self._get_etree_from_bpmn_file(bpmn_file) + parser.add_bpmn_xml(bpmn_file_etree, filename=os.path.basename(bpmn_file)) + all_related = self._find_related_bpmn_files(bpmn_file) + for related_file in all_related: + related_file_etree = self._get_etree_from_bpmn_file(related_file) + parser.add_bpmn_xml(related_file_etree, filename=os.path.basename(related_file)) + sub_parsers = list(parser.process_parsers.values()) + executable_process = None + for sub_parser in sub_parsers: + if sub_parser.process_executable: + executable_process = sub_parser.bpmn_id + if executable_process is None: + raise BpmnFileMissingExecutableProcessError( + f"Executable process cannot be found in {bpmn_file}. Test cannot run." + ) + bpmn_process_spec = parser.get_spec(executable_process) + bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) + return bpmn_process_instance + + def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error: Optional[str] = None) -> None: + bpmn_file_relative = os.path.relpath(bpmn_file, start=self.process_model_directory_path) + test_result = TestCaseResult( + passed=passed, + bpmn_file=bpmn_file_relative, + test_case_name=test_case_name, + error=error, + ) + self.test_case_results.append(test_result) + class BpmnFileMissingExecutableProcessError(Exception): pass @@ -149,9 +267,9 @@ class ProcessModelTestRunnerService: def __init__(self, process_model_directory_path: str) -> None: self.process_model_test_runner = ProcessModelTestRunner( process_model_directory_path, - instantiate_executer_callback=self._instantiate_executer_callback, - execute_task_callback=self._execute_task_callback, - get_next_task_callback=self._get_next_task_callback, + # instantiate_executer_callback=self._instantiate_executer_callback, + # execute_task_callback=self._execute_task_callback, + # get_next_task_callback=self._get_next_task_callback, ) def run(self) -> None: @@ -168,7 +286,6 @@ class ProcessModelTestRunnerService: def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) - if len(tasks) > 0: tasks = [tasks[0]] @@ -179,7 +296,8 @@ class ProcessModelTestRunnerService: data = None with open(bpmn_file, "rb") as f_handle: data = f_handle.read() - bpmn: etree.Element = SpecFileService.get_etree_from_xml_bytes(data) + etree_xml_parser = etree.XMLParser(resolve_entities=False) + bpmn = etree.fromstring(data, parser=etree_xml_parser) parser.add_bpmn_xml(bpmn, filename=os.path.basename(bpmn_file)) sub_parsers = list(parser.process_parsers.values()) executable_process = None diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn new file mode 100644 index 00000000..ab796146 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn @@ -0,0 +1,40 @@ + + + + + Flow_0ext5lt + + + + Flow_1hzwssi + + + + + Flow_0ext5lt + Flow_1hzwssi + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json new file mode 100644 index 00000000..8f4e4a04 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "", + "display_name": "Basic Call Activity", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "basic_call_activity.bpmn", + "primary_process_id": "BasicCallActivityProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json new file mode 100644 index 00000000..0a7f7692 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json @@ -0,0 +1,5 @@ +{ + "test_case_one": { + "expected_output_json": {} + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn new file mode 100644 index 00000000..5d0bf395 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn @@ -0,0 +1,39 @@ + + + + + Flow_0gz6i84 + + + + Flow_0ikklg6 + + + + Flow_0gz6i84 + Flow_0ikklg6 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json new file mode 100644 index 00000000..743dd104 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "Baisc Manual Task", + "display_name": "Baisc Manual Task", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "baisc_manual_task.bpmn", + "primary_process_id": "BasicManualTaskProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json new file mode 100644 index 00000000..d82f5c7c --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json @@ -0,0 +1,10 @@ +{ + "test_case_one": { + "tasks": { + "manual_task_one": { + "data": {} + } + }, + "expected_output_json": {} + } +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py index a993c945..89ed9ec7 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -9,7 +9,7 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunnerService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService class TestProcessModelTestRunnerService(BaseTest): @@ -23,7 +23,7 @@ class TestProcessModelTestRunnerService(BaseTest): os.path.join(FileSystemService.root_path(), "basic_script_task") ) test_runner_service.run() - assert test_runner_service.process_model_test_runner.all_test_cases_passed() + assert test_runner_service.process_model_test_runner.all_test_cases_passed(), test_runner_service.process_model_test_runner.test_case_results def test_can_test_multiple_process_models( self, @@ -35,6 +35,19 @@ class TestProcessModelTestRunnerService(BaseTest): test_runner_service.run() assert test_runner_service.process_model_test_runner.all_test_cases_passed() is False + def test_can_test_process_model_call_activity( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + test_runner_service = ProcessModelTestRunner( + process_model_directory_path=FileSystemService.root_path(), + process_model_directory_for_test_discovery=os.path.join(FileSystemService.root_path(), "basic_call_activity") + ) + test_runner_service.run() + assert test_runner_service.all_test_cases_passed() is True, test_runner_service.test_case_results + @pytest.fixture() def with_mocked_root_path(self, mocker: MockerFixture) -> None: path = os.path.join( From 40c67f000ca119a11df4ce7cd9e7b4f8c9a2f5a0 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 17:28:51 -0400 Subject: [PATCH 06/60] cleaned up process model tests and added support for service tasks w/ burnettk --- .../process_model_test_runner_service.py | 58 ++-------- .../basic_failing_script_task.bpmn | 0 .../process_model.json | 0 .../test_basic_failing_script_task.json | 0 .../basic_call_activity.bpmn | 0 .../basic_call_activity/process_model.json | 0 .../test_basic_call_activity.json | 0 .../basic_manual_task/basic_manual_task.bpmn | 0 .../basic_manual_task/process_model.json | 0 .../test_basic_manual_task.json | 0 .../basic_script_task/basic_script_task.bpmn | 0 .../basic_script_task/process_model.json | 0 .../test_basic_script_task.json | 0 .../basic_service_task.bpmn | 56 ++++++++++ .../basic_service_task/process_model.json | 10 ++ .../test_basic_service_task.json | 10 ++ .../unit/test_process_model_test_runner.py | 100 ++++++++++++++++++ .../test_process_model_test_runner_service.py | 61 ----------- 18 files changed, 186 insertions(+), 109 deletions(-) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => failing_tests}/basic_failing_script_task/basic_failing_script_task.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => failing_tests}/basic_failing_script_task/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => failing_tests}/basic_failing_script_task/test_basic_failing_script_task.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_call_activity/basic_call_activity.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_call_activity/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_call_activity/test_basic_call_activity.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_manual_task/basic_manual_task.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_manual_task/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_manual_task/test_basic_manual_task.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_script_task/basic_script_task.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_script_task/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_script_task/test_basic_script_task.json (100%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json create mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py delete mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 4093e2f6..7a5569e6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -36,6 +36,10 @@ class MissingBpmnFileForTestCaseError(Exception): pass +class NoTestCasesFoundError(Exception): + pass + + @dataclass class TestCaseResult: passed: bool @@ -95,6 +99,8 @@ class ProcessModelTestRunner: return len(failed_tests) < 1 def run(self) -> None: + if len(self.test_mappings.items()) < 1: + raise NoTestCasesFoundError(f"Could not find any test cases in given directory: {self.process_model_directory_for_test_discovery}") for json_test_case_file, bpmn_file in self.test_mappings.items(): with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) @@ -189,21 +195,14 @@ class ProcessModelTestRunner: return self.instantiate_executer_callback(bpmn_file) return self._default_instantiate_executer(bpmn_file) - def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: - tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) - if len(tasks) > 0: - tasks = [tasks[0]] - - return tasks - def _default_get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: - engine_steps = self._get_ready_engine_steps(bpmn_process_instance) - if len(engine_steps) > 0: - return engine_steps[0] + ready_tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY)]) + if len(ready_tasks) > 0: + return ready_tasks[0] return None def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: - if spiff_task.task_spec.manual: + if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == 'ServiceTask': if test_case_task_properties and 'data' in test_case_task_properties: spiff_task.update_data(test_case_task_properties['data']) spiff_task.complete() @@ -274,40 +273,3 @@ class ProcessModelTestRunnerService: def run(self) -> None: self.process_model_test_runner.run() - - def _execute_task_callback(self, spiff_task: SpiffTask, _test_case_json: Optional[dict]) -> None: - spiff_task.run() - - def _get_next_task_callback(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: - engine_steps = self._get_ready_engine_steps(bpmn_process_instance) - if len(engine_steps) > 0: - return engine_steps[0] - return None - - def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: - tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) - if len(tasks) > 0: - tasks = [tasks[0]] - - return tasks - - def _instantiate_executer_callback(self, bpmn_file: str) -> BpmnWorkflow: - parser = MyCustomParser() - data = None - with open(bpmn_file, "rb") as f_handle: - data = f_handle.read() - etree_xml_parser = etree.XMLParser(resolve_entities=False) - bpmn = etree.fromstring(data, parser=etree_xml_parser) - parser.add_bpmn_xml(bpmn, filename=os.path.basename(bpmn_file)) - sub_parsers = list(parser.process_parsers.values()) - executable_process = None - for sub_parser in sub_parsers: - if sub_parser.process_executable: - executable_process = sub_parser.bpmn_id - if executable_process is None: - raise BpmnFileMissingExecutableProcessError( - f"Executable process cannot be found in {bpmn_file}. Test cannot run." - ) - bpmn_process_spec = parser.get_spec(executable_process) - bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) - return bpmn_process_instance diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn new file mode 100644 index 00000000..8675d591 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn @@ -0,0 +1,56 @@ + + + + + Flow_19ephzh + + + + Flow_1dsxn78 + + + + + + + + + + + + + + This is the Service Task Unit Test Screen. + + + Flow_0xx2kop + Flow_1dsxn78 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json new file mode 100644 index 00000000..0946afc6 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json @@ -0,0 +1,10 @@ +{ + "description": "A.1.0.2", + "display_name": "A.1.0.2 - Service Task", + "display_order": 13, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "A.1.0.2.bpmn", + "primary_process_id": "Process_test_a102_A_1_0_2_bd2e724" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json new file mode 100644 index 00000000..729d81ac --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json @@ -0,0 +1,10 @@ +{ + "test_case_one": { + "tasks": { + "service_task_one": { + "data": { "the_result": "result_from_service" } + } + }, + "expected_output_json": { "the_result": "result_from_service" } + } +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py new file mode 100644 index 00000000..a66c9f8a --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -0,0 +1,100 @@ +import os +from typing import Any +from typing import Optional + +import pytest +from flask import current_app +from flask import Flask +from pytest_mock import MockerFixture +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + +from spiffworkflow_backend.models.task import TaskModel # noqa: F401 +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError, ProcessModelTestRunner + + +class TestProcessModelTestRunner(BaseTest): + def test_can_test_a_simple_process_model( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests('basic_script_task') + assert len(process_model_test_runner.test_case_results) == 1 + + def test_will_raise_if_no_tests_found( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = ProcessModelTestRunner( + os.path.join(FileSystemService.root_path(), "DNE") + ) + with pytest.raises(NoTestCasesFoundError): + process_model_test_runner.run() + assert process_model_test_runner.all_test_cases_passed(), process_model_test_runner.test_case_results + + def test_can_test_multiple_process_models_with_all_passing_tests( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests() + assert len(process_model_test_runner.test_case_results) > 1 + + def test_can_test_multiple_process_models_with_failing_tests( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(parent_directory='failing_tests') + assert len(process_model_test_runner.test_case_results) == 1 + + def test_can_test_process_model_call_activity( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_call_activity') + assert len(process_model_test_runner.test_case_results) == 1 + + def test_can_test_process_model_with_service_task( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_service_task') + assert len(process_model_test_runner.test_case_results) == 1 + + def _run_model_tests(self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = 'passing_tests') -> ProcessModelTestRunner: + base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] + path_segments = base_process_model_dir_path_segments + if bpmn_process_directory_name: + path_segments = path_segments + [bpmn_process_directory_name] + process_model_test_runner = ProcessModelTestRunner( + process_model_directory_path=os.path.join(*base_process_model_dir_path_segments), + process_model_directory_for_test_discovery=os.path.join(*path_segments) + ) + process_model_test_runner.run() + + all_tests_expected_to_pass = parent_directory == 'passing_tests' + assert process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass, process_model_test_runner.test_case_results + return process_model_test_runner + + @pytest.fixture() + def with_mocked_root_path(self, mocker: MockerFixture) -> None: + path = os.path.join( + current_app.instance_path, + "..", + "..", + "tests", + "data", + "bpmn_unit_test_process_models", + ) + mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py deleted file mode 100644 index 89ed9ec7..00000000 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -from typing import Any - -import pytest -from flask import current_app -from flask import Flask -from pytest_mock import MockerFixture -from tests.spiffworkflow_backend.helpers.base_test import BaseTest - -from spiffworkflow_backend.models.task import TaskModel # noqa: F401 -from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService - - -class TestProcessModelTestRunnerService(BaseTest): - def test_can_test_a_simple_process_model( - self, - app: Flask, - with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, - ) -> None: - test_runner_service = ProcessModelTestRunnerService( - os.path.join(FileSystemService.root_path(), "basic_script_task") - ) - test_runner_service.run() - assert test_runner_service.process_model_test_runner.all_test_cases_passed(), test_runner_service.process_model_test_runner.test_case_results - - def test_can_test_multiple_process_models( - self, - app: Flask, - with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, - ) -> None: - test_runner_service = ProcessModelTestRunnerService(FileSystemService.root_path()) - test_runner_service.run() - assert test_runner_service.process_model_test_runner.all_test_cases_passed() is False - - def test_can_test_process_model_call_activity( - self, - app: Flask, - with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, - ) -> None: - test_runner_service = ProcessModelTestRunner( - process_model_directory_path=FileSystemService.root_path(), - process_model_directory_for_test_discovery=os.path.join(FileSystemService.root_path(), "basic_call_activity") - ) - test_runner_service.run() - assert test_runner_service.all_test_cases_passed() is True, test_runner_service.test_case_results - - @pytest.fixture() - def with_mocked_root_path(self, mocker: MockerFixture) -> None: - path = os.path.join( - current_app.instance_path, - "..", - "..", - "tests", - "data", - "bpmn_unit_test_process_models", - ) - mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) From 5b793d5a817fcfa328ad1fbd7d9928bc3b37d839 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 10:02:07 -0400 Subject: [PATCH 07/60] added parse method to turn the yaml into the same format as the incoming perms from the dmn tables w/ burnettk --- .../scripts/refresh_permissions.py | 1 - .../services/authorization_service.py | 63 ++++++++++++++++--- .../unit/test_authorization_service.py | 4 +- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py index 4981af93..4a49d7b5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py @@ -34,6 +34,5 @@ class RefreshPermissions(Script): *args: Any, **kwargs: Any, ) -> Any: - """Run.""" group_info = args[0] AuthorizationService.refresh_permissions(group_info) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 761f14a2..ccf51cd8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -94,13 +94,22 @@ class UserToGroupDict(TypedDict): class DesiredPermissionDict(TypedDict): - """DesiredPermissionDict.""" - group_identifiers: Set[str] permission_assignments: list[PermissionAssignmentModel] user_to_group_identifiers: list[UserToGroupDict] +class DesiredGroupPermissionDict(TypedDict): + actions: list[str] + uri: str + + +class GroupPermissionsDict(TypedDict): + users: list[str] + name: str + permissions: list[DesiredGroupPermissionDict] + + class AuthorizationService: """Determine whether a user has permission to perform their request.""" @@ -699,17 +708,57 @@ class AuthorizationService: return permission_assignments @classmethod - def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None: + def parse_permissions_yaml_into_group_info(cls) -> list[GroupPermissionsDict]: + if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None: + raise ( + PermissionsFileNotSetError( + "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in order to import permissions" + ) + ) + + permission_configs = None + with open(current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_ABSOLUTE_PATH"]) as file: + permission_configs = yaml.safe_load(file) + + group_permissions: list[GroupPermissionsDict] = [] + group_permissions_by_group: dict[str, GroupPermissionsDict] = {} + if "default_group" in permission_configs: + default_group_identifier = permission_configs["default_group"] + # group_permissions.append({"name": default_group_identifier, "users": [], "permissions": []}) + group_permissions_by_group[default_group_identifier] = {"name": default_group_identifier, "users": [], "permissions": []} + + if "groups" in permission_configs: + for group_identifier, group_config in permission_configs["groups"].items(): + group_info: GroupPermissionsDict = {"name": group_identifier, "users": [], "permissions": []} + for username in group_config["users"]: + group_info['users'].append(username) + group_permissions_by_group[group_identifier] = group_info + + if "permissions" in permission_configs: + for _permission_identifier, permission_config in permission_configs["permissions"].items(): + uri = permission_config["uri"] + for group_identifier in permission_config["groups"]: + group_permissions_by_group[group_identifier]['permissions'].append({'actions': permission_config["allowed_permissions"], "uri": uri}) + + for _group_identifier, group_permission in group_permissions_by_group.items(): + group_permissions.append(group_permission) + + return group_permissions + + @classmethod + def refresh_permissions(cls, group_info: list[GroupPermissionsDict]) -> None: """Adds new permission assignments and deletes old ones.""" initial_permission_assignments = PermissionAssignmentModel.query.all() initial_user_to_group_assignments = UserGroupAssignmentModel.query.all() - result = cls.import_permissions_from_yaml_file() - desired_permission_assignments = result["permission_assignments"] - desired_group_identifiers = result["group_identifiers"] - desired_user_to_group_identifiers = result["user_to_group_identifiers"] + group_info = group_info + cls.parse_permissions_yaml_into_group_info() + + desired_group_identifiers: Set[str] = set() + desired_permission_assignments: list[PermissionAssignmentModel] = [] + desired_user_to_group_identifiers: list[UserToGroupDict] = [] for group in group_info: group_identifier = group["name"] + GroupService.find_or_create_group(group_identifier) for username in group["users"]: user_to_group_dict: UserToGroupDict = { "username": username, diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index b742c463..5712cb62 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -7,7 +7,7 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserNotFoundError -from spiffworkflow_backend.services.authorization_service import AuthorizationService +from spiffworkflow_backend.services.authorization_service import AuthorizationService, GroupPermissionsDict from spiffworkflow_backend.services.authorization_service import InvalidPermissionError from spiffworkflow_backend.services.group_service import GroupService from spiffworkflow_backend.services.process_instance_processor import ( @@ -399,7 +399,7 @@ class TestAuthorizationService(BaseTest): GroupService.find_or_create_group("group_three") assert GroupModel.query.filter_by(identifier="group_three").first() is not None - group_info = [ + group_info: list[GroupPermissionsDict] = [ { "users": ["user_one", "user_two"], "name": "group_one", From 84f3847c507f3d28165b4f4ccf932adf7ffe0b8c Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 11:29:15 -0400 Subject: [PATCH 08/60] refactored import perms from yaml and from dmn tables to do the same thing w/ burnettk --- .../spiffworkflow_backend/config/default.py | 1 + .../config/permissions/unit_testing.yml | 10 -- .../scripts/refresh_permissions.py | 1 + .../services/authorization_service.py | 163 ++++++------------ .../services/group_service.py | 5 - .../unit/test_authorization_service.py | 18 +- 6 files changed, 59 insertions(+), 139 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index 88a34faf..08187f7e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -143,6 +143,7 @@ SPIFFWORKFLOW_BACKEND_ALLOW_CONFISCATING_LOCK_AFTER_SECONDS = int( environ.get("SPIFFWORKFLOW_BACKEND_ALLOW_CONFISCATING_LOCK_AFTER_SECONDS", default="600") ) +# FIXME: do not default this but we will need to coordinate release of it since it is a breaking change SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP = environ.get("SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP", default="everybody") SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_BACKGROUND = environ.get( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml index d3edf0a8..6cabb4b0 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml @@ -1,5 +1,3 @@ -default_group: everybody - users: testadmin1: service: https://testing/openid/thing @@ -20,49 +18,41 @@ groups: permissions: admin: groups: [admin] - users: [] allowed_permissions: [create, read, update, delete] uri: /* read-all: groups: ["Finance Team", hr, admin] - users: [] allowed_permissions: [read] uri: /* process-instances-find-by-id: groups: [everybody] - users: [] allowed_permissions: [read] uri: /process-instances/find-by-id/* tasks-crud: groups: [everybody] - users: [] allowed_permissions: [create, read, update, delete] uri: /tasks/* # TODO: all uris should really have the same structure finance-admin-group: groups: ["Finance Team"] - users: [testuser4] allowed_permissions: [create, read, update, delete] uri: /process-groups/finance/* finance-admin-model: groups: ["Finance Team"] - users: [testuser4] allowed_permissions: [create, read, update, delete] uri: /process-models/finance/* finance-admin-model-lanes: groups: ["Finance Team"] - users: [testuser4] allowed_permissions: [create, read, update, delete] uri: /process-models/finance:model_with_lanes/* finance-admin-instance-run: groups: ["Finance Team"] - users: [testuser4] allowed_permissions: [create, read, update, delete] uri: /process-instances/* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py index 4a49d7b5..c8192574 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py @@ -35,4 +35,5 @@ class RefreshPermissions(Script): **kwargs: Any, ) -> Any: group_info = args[0] + import pdb; pdb.set_trace() AuthorizationService.refresh_permissions(group_info) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index ccf51cd8..61e4d25c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -21,6 +21,7 @@ from sqlalchemy import or_ from sqlalchemy import text from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX +from spiffworkflow_backend.models import permission_assignment from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.human_task import HumanTaskModel @@ -196,88 +197,13 @@ class AuthorizationService: db.session.commit() @classmethod - def import_permissions_from_yaml_file(cls, raise_if_missing_user: bool = False) -> DesiredPermissionDict: - """Import_permissions_from_yaml_file.""" - if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None: - raise ( - PermissionsFileNotSetError( - "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME needs to be set in order to import permissions" - ) - ) - - permission_configs = None - with open(current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_ABSOLUTE_PATH"]) as file: - permission_configs = yaml.safe_load(file) - - default_group = None - unique_user_group_identifiers: Set[str] = set() - user_to_group_identifiers: list[UserToGroupDict] = [] - if "default_group" in permission_configs: - default_group_identifier = permission_configs["default_group"] - default_group = GroupService.find_or_create_group(default_group_identifier) - unique_user_group_identifiers.add(default_group_identifier) - - if "groups" in permission_configs: - for group_identifier, group_config in permission_configs["groups"].items(): - group = GroupService.find_or_create_group(group_identifier) - unique_user_group_identifiers.add(group_identifier) - for username in group_config["users"]: - user = UserModel.query.filter_by(username=username).first() - if user is None: - if raise_if_missing_user: - raise (UserNotFoundError(f"Could not find a user with name: {username}")) - continue - user_to_group_dict: UserToGroupDict = { - "username": user.username, - "group_identifier": group_identifier, - } - user_to_group_identifiers.append(user_to_group_dict) - cls.associate_user_with_group(user, group) - - permission_assignments = [] - if "permissions" in permission_configs: - for _permission_identifier, permission_config in permission_configs["permissions"].items(): - uri = permission_config["uri"] - permission_target = cls.find_or_create_permission_target(uri) - - for allowed_permission in permission_config["allowed_permissions"]: - if "groups" in permission_config: - for group_identifier in permission_config["groups"]: - group = GroupService.find_or_create_group(group_identifier) - unique_user_group_identifiers.add(group_identifier) - permission_assignments.append( - cls.create_permission_for_principal( - group.principal, - permission_target, - allowed_permission, - ) - ) - if "users" in permission_config: - for username in permission_config["users"]: - user = UserModel.query.filter_by(username=username).first() - if user is not None: - principal = ( - PrincipalModel.query.join(UserModel).filter(UserModel.username == username).first() - ) - permission_assignments.append( - cls.create_permission_for_principal( - principal, permission_target, allowed_permission - ) - ) - - if default_group is not None: - for user in UserModel.query.all(): - cls.associate_user_with_group(user, default_group) - - return { - "group_identifiers": unique_user_group_identifiers, - "permission_assignments": permission_assignments, - "user_to_group_identifiers": user_to_group_identifiers, - } + def import_permissions_from_yaml_file(cls, user_model: Optional[UserModel] = None) -> DesiredPermissionDict: + group_permissions = cls.parse_permissions_yaml_into_group_info() + result = cls.add_permissions_from_group_permissions(group_permissions, user_model) + return result @classmethod def find_or_create_permission_target(cls, uri: str) -> PermissionTargetModel: - """Find_or_create_permission_target.""" uri_with_percent = re.sub(r"\*", "%", uri) target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX) permission_target: Optional[PermissionTargetModel] = PermissionTargetModel.query.filter_by( @@ -296,7 +222,6 @@ class AuthorizationService: permission_target: PermissionTargetModel, permission: str, ) -> PermissionAssignmentModel: - """Create_permission_for_principal.""" permission_assignment: Optional[PermissionAssignmentModel] = PermissionAssignmentModel.query.filter_by( principal_id=principal.id, permission_target_id=permission_target.id, @@ -315,7 +240,6 @@ class AuthorizationService: @classmethod def should_disable_auth_for_request(cls) -> bool: - """Should_disable_auth_for_request.""" swagger_functions = ["get_json_spec"] authentication_exclusion_list = [ "status", @@ -353,7 +277,6 @@ class AuthorizationService: @classmethod def get_permission_from_http_method(cls, http_method: str) -> Optional[str]: - """Get_permission_from_request_method.""" request_method_mapper = { "POST": "create", "GET": "read", @@ -515,7 +438,7 @@ class AuthorizationService: # we are also a little apprehensive about pre-creating users # before the user signs in, because we won't know things like # the external service user identifier. - cls.import_permissions_from_yaml_file() + cls.import_permissions_from_yaml_file(user_model) if is_new_user: UserService.add_user_to_human_tasks_if_appropriate(user_model) @@ -720,11 +643,9 @@ class AuthorizationService: with open(current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_ABSOLUTE_PATH"]) as file: permission_configs = yaml.safe_load(file) - group_permissions: list[GroupPermissionsDict] = [] group_permissions_by_group: dict[str, GroupPermissionsDict] = {} - if "default_group" in permission_configs: - default_group_identifier = permission_configs["default_group"] - # group_permissions.append({"name": default_group_identifier, "users": [], "permissions": []}) + if current_app.config['SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP']: + default_group_identifier = current_app.config['SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP'] group_permissions_by_group[default_group_identifier] = {"name": default_group_identifier, "users": [], "permissions": []} if "groups" in permission_configs: @@ -738,25 +659,25 @@ class AuthorizationService: for _permission_identifier, permission_config in permission_configs["permissions"].items(): uri = permission_config["uri"] for group_identifier in permission_config["groups"]: - group_permissions_by_group[group_identifier]['permissions'].append({'actions': permission_config["allowed_permissions"], "uri": uri}) + group_permissions_by_group[group_identifier]['permissions'].append( + {'actions': permission_config["allowed_permissions"], "uri": uri} + ) - for _group_identifier, group_permission in group_permissions_by_group.items(): - group_permissions.append(group_permission) - - return group_permissions + return list(group_permissions_by_group.values()) @classmethod - def refresh_permissions(cls, group_info: list[GroupPermissionsDict]) -> None: - """Adds new permission assignments and deletes old ones.""" - initial_permission_assignments = PermissionAssignmentModel.query.all() - initial_user_to_group_assignments = UserGroupAssignmentModel.query.all() - group_info = group_info + cls.parse_permissions_yaml_into_group_info() + def add_permissions_from_group_permissions(cls, group_permissions: list[GroupPermissionsDict], user_model: Optional[UserModel] = None) -> DesiredPermissionDict: + unique_user_group_identifiers: Set[str] = set() + user_to_group_identifiers: list[UserToGroupDict] = [] + permission_assignments = [] - desired_group_identifiers: Set[str] = set() - desired_permission_assignments: list[PermissionAssignmentModel] = [] - desired_user_to_group_identifiers: list[UserToGroupDict] = [] + default_group = None + default_group_identifier = current_app.config['SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP'] + if default_group_identifier: + default_group = GroupService.find_or_create_group(default_group_identifier) + unique_user_group_identifiers.add(default_group_identifier) - for group in group_info: + for group in group_permissions: group_identifier = group["name"] GroupService.find_or_create_group(group_identifier) for username in group["users"]: @@ -764,19 +685,44 @@ class AuthorizationService: "username": username, "group_identifier": group_identifier, } - desired_user_to_group_identifiers.append(user_to_group_dict) + user_to_group_identifiers.append(user_to_group_dict) GroupService.add_user_to_group_or_add_to_waiting(username, group_identifier) - desired_group_identifiers.add(group_identifier) + unique_user_group_identifiers.add(group_identifier) for permission in group["permissions"]: for crud_op in permission["actions"]: - desired_permission_assignments.extend( + permission_assignments.extend( cls.add_permission_from_uri_or_macro( group_identifier=group_identifier, target=permission["uri"], permission=crud_op, ) ) - desired_group_identifiers.add(group_identifier) + unique_user_group_identifiers.add(group_identifier) + + if default_group is not None: + if user_model: + cls.associate_user_with_group(user_model, default_group) + else: + for user in UserModel.query.all(): + cls.associate_user_with_group(user, default_group) + + return { + "group_identifiers": unique_user_group_identifiers, + "permission_assignments": permission_assignments, + "user_to_group_identifiers": user_to_group_identifiers, + } + + @classmethod + def refresh_permissions(cls, group_permissions: list[GroupPermissionsDict]) -> None: + """Adds new permission assignments and deletes old ones.""" + initial_permission_assignments = PermissionAssignmentModel.query.all() + initial_user_to_group_assignments = UserGroupAssignmentModel.query.all() + group_permissions = group_permissions + cls.parse_permissions_yaml_into_group_info() + + result = cls.add_permissions_from_group_permissions(group_permissions) + desired_permission_assignments = result["permission_assignments"] + desired_group_identifiers = result["group_identifiers"] + desired_user_to_group_identifiers = result["user_to_group_identifiers"] for ipa in initial_permission_assignments: if ipa not in desired_permission_assignments: @@ -801,10 +747,3 @@ class AuthorizationService: for gtd in groups_to_delete: db.session.delete(gtd) db.session.commit() - - -class KeycloakAuthorization: - """Interface with Keycloak server.""" - - -# class KeycloakClient: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py index eee47bc6..c1938cfb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/group_service.py @@ -1,4 +1,3 @@ -"""Group_service.""" from typing import Optional from spiffworkflow_backend.models.db import db @@ -8,11 +7,8 @@ from spiffworkflow_backend.services.user_service import UserService class GroupService: - """GroupService.""" - @classmethod def find_or_create_group(cls, group_identifier: str) -> GroupModel: - """Find_or_create_group.""" group: Optional[GroupModel] = GroupModel.query.filter_by(identifier=group_identifier).first() if group is None: group = GroupModel(identifier=group_identifier) @@ -23,7 +19,6 @@ class GroupService: @classmethod def add_user_to_group_or_add_to_waiting(cls, username: str, group_identifier: str) -> None: - """Add_user_to_group_or_add_to_waiting.""" group = cls.find_or_create_group(group_identifier) user = UserModel.query.filter_by(username=username).first() if user: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 5712cb62..380677ae 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -21,19 +21,10 @@ from spiffworkflow_backend.services.user_service import UserService class TestAuthorizationService(BaseTest): - """TestAuthorizationService.""" - - def test_can_raise_if_missing_user(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None: - """Test_can_raise_if_missing_user.""" - with pytest.raises(UserNotFoundError): - AuthorizationService.import_permissions_from_yaml_file(raise_if_missing_user=True) - def test_does_not_fail_if_user_not_created(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None: - """Test_does_not_fail_if_user_not_created.""" AuthorizationService.import_permissions_from_yaml_file() def test_can_import_permissions_from_yaml(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None: - """Test_can_import_permissions_from_yaml.""" usernames = [ "testadmin1", "testadmin2", @@ -59,8 +50,6 @@ class TestAuthorizationService(BaseTest): self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance/model1") self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance/") self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/", expected_result=False) - self.assert_user_has_permission(users["testuser4"], "update", "/v1.0/process-groups/finance/model1") - # via the user, not the group self.assert_user_has_permission(users["testuser4"], "read", "/v1.0/process-groups/finance/model1") self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/finance/model1") self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/", expected_result=False) @@ -387,7 +376,6 @@ class TestAuthorizationService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - """Test_can_refresh_permissions.""" user = self.find_or_create_user(username="user_one") user_two = self.find_or_create_user(username="user_two") admin_user = self.find_or_create_user(username="testadmin1") @@ -410,6 +398,11 @@ class TestAuthorizationService(BaseTest): "name": "group_three", "permissions": [{"actions": ["create", "read"], "uri": "PG:hey2"}], }, + { + "users": [], + "name": "everybody", + "permissions": [{"actions": ["read"], "uri": "PG:hey2everybody"}], + }, ] AuthorizationService.refresh_permissions(group_info) assert GroupModel.query.filter_by(identifier="group_two").first() is None @@ -418,6 +411,7 @@ class TestAuthorizationService(BaseTest): self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey") self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo") self.assert_user_has_permission(user, "create", "/v1.0/process-groups/hey:yo") + self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey2everybody:yo") self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey") self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey:yo") From 40b3246eb70c77a2e9edd30426a5e9fbdbfd7f53 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 12:11:40 -0400 Subject: [PATCH 09/60] support macros in perm yml and pyl --- .../config/permissions/acceptance_tests.yml | 1 - .../config/permissions/demo.yml | 3 --- .../config/permissions/example.yml | 10 ------- .../config/permissions/example_read_only.yml | 14 ---------- .../config/permissions/local_development.yml | 2 -- .../terraform_deployed_environment.yml | 2 -- .../config/permissions/unit_testing.yml | 13 +++++----- .../scripts/refresh_permissions.py | 1 - .../services/authorization_service.py | 26 ++++++++++--------- .../integration/test_process_api.py | 8 ------ .../unit/test_authorization_service.py | 17 ++++++------ 11 files changed, 28 insertions(+), 69 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml index 0382f389..f3f84773 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/acceptance_tests.yml @@ -12,6 +12,5 @@ groups: permissions: admin: groups: [admin] - users: [] allowed_permissions: [create, read, update, delete] uri: /* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/demo.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/demo.yml index dbded55b..1b1d161e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/demo.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/demo.yml @@ -1,5 +1,3 @@ -default_group: everybody - groups: admin: users: @@ -19,6 +17,5 @@ groups: permissions: admin: groups: [admin, tech_writers] - users: [] allowed_permissions: [create, read, update, delete] uri: /* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml index a11578bd..0684ef55 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml @@ -1,4 +1,3 @@ -default_group: everybody users: admin: @@ -41,52 +40,43 @@ permissions: # Admins have access to everything. admin: groups: [admin] - users: [] allowed_permissions: [create, read, update, delete] uri: /* # Everybody can participate in tasks assigned to them. tasks-crud: groups: [everybody] - users: [] allowed_permissions: [create, read, update, delete] uri: /tasks/* # Everybody can start all intstances create-test-instances: groups: [ everybody ] - users: [ ] allowed_permissions: [ create ] uri: /process-instances/* # Everyone can see everything (all groups, and processes are visible) read-all-process-groups: groups: [ everybody ] - users: [ ] allowed_permissions: [ read ] uri: /process-groups/* read-all-process-models: groups: [ everybody ] - users: [ ] allowed_permissions: [ read ] uri: /process-models/* read-all-process-instance: groups: [ everybody ] - users: [ ] allowed_permissions: [ read ] uri: /process-instances/* read-process-instance-reports: groups: [ everybody ] - users: [ ] allowed_permissions: [ read ] uri: /process-instances/reports/* processes-read: groups: [ everybody ] - users: [ ] allowed_permissions: [ read ] uri: /processes groups-everybody: groups: [everybody] - users: [] allowed_permissions: [create, read] uri: /v1.0/user-groups/for-current-user diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml index d201a555..b6facc64 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml @@ -1,4 +1,3 @@ -default_group: everybody groups: admin: @@ -7,78 +6,65 @@ groups: permissions: admin: groups: [admin] - users: [] allowed_permissions: [read] uri: /* tasks-crud: groups: [admin] - users: [] allowed_permissions: [create, update, delete] uri: /tasks/* process-instances-crud: groups: [ admin ] - users: [ ] allowed_permissions: [create, update, delete] uri: /process-instances/* suspend: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/process-instance-suspend terminate: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/process-instance-terminate resume: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/process-instance-resume reset: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/process-instance-reset users-exist: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/users/exists/by-username send-event: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/send-event/* task-complete: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/task-complete/* messages: groups: [admin] - users: [] allowed_permissions: [create] uri: /v1.0/messages/* secrets: groups: [admin] - users: [] allowed_permissions: [create, update, delete] uri: /v1.0/secrets/* task-data: groups: [admin] - users: [] allowed_permissions: [update] uri: /v1.0/task-data/* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml index eb9ce4b7..4b596568 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/local_development.yml @@ -1,4 +1,3 @@ -default_group: everybody groups: admin: @@ -11,6 +10,5 @@ groups: permissions: admin: groups: [admin, group1, group2] - users: [] allowed_permissions: [create, read, update, delete] uri: /* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml index 049c991e..14876bf7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/terraform_deployed_environment.yml @@ -1,4 +1,3 @@ -default_group: everybody groups: admin: @@ -7,6 +6,5 @@ groups: permissions: admin: groups: [admin] - users: [] allowed_permissions: [create, read, update, delete] uri: /* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml index 6cabb4b0..10980462 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml @@ -36,16 +36,15 @@ permissions: allowed_permissions: [create, read, update, delete] uri: /tasks/* - # TODO: all uris should really have the same structure finance-admin-group: groups: ["Finance Team"] - allowed_permissions: [create, read, update, delete] - uri: /process-groups/finance/* + allowed_permissions: [all] + uri: PG:finance - finance-admin-model: - groups: ["Finance Team"] - allowed_permissions: [create, read, update, delete] - uri: /process-models/finance/* + finance-hr-start: + groups: ["hr"] + allowed_permissions: [start] + uri: PG:finance finance-admin-model-lanes: groups: ["Finance Team"] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py index c8192574..4a49d7b5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/refresh_permissions.py @@ -35,5 +35,4 @@ class RefreshPermissions(Script): **kwargs: Any, ) -> Any: group_info = args[0] - import pdb; pdb.set_trace() AuthorizationService.refresh_permissions(group_info) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 61e4d25c..32efe6d8 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from hashlib import sha256 from hmac import compare_digest from hmac import HMAC -from typing import Any from typing import Optional from typing import Set from typing import TypedDict @@ -21,7 +20,6 @@ from sqlalchemy import or_ from sqlalchemy import text from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX -from spiffworkflow_backend.models import permission_assignment from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.human_task import HumanTaskModel @@ -30,7 +28,6 @@ from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.principal import MissingPrincipalError from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.models.user import UserNotFoundError from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.routes.openid_blueprint import openid_blueprint from spiffworkflow_backend.services.authentication_service import NotAuthorizedError @@ -617,7 +614,6 @@ class AuthorizationService: def add_permission_from_uri_or_macro( cls, group_identifier: str, permission: str, target: str ) -> list[PermissionAssignmentModel]: - """Add_permission_from_uri_or_macro.""" group = GroupService.find_or_create_group(group_identifier) permissions_to_assign = cls.explode_permissions(permission, target) permission_assignments = [] @@ -644,35 +640,41 @@ class AuthorizationService: permission_configs = yaml.safe_load(file) group_permissions_by_group: dict[str, GroupPermissionsDict] = {} - if current_app.config['SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP']: - default_group_identifier = current_app.config['SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP'] - group_permissions_by_group[default_group_identifier] = {"name": default_group_identifier, "users": [], "permissions": []} + if current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]: + default_group_identifier = current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"] + group_permissions_by_group[default_group_identifier] = { + "name": default_group_identifier, + "users": [], + "permissions": [], + } if "groups" in permission_configs: for group_identifier, group_config in permission_configs["groups"].items(): group_info: GroupPermissionsDict = {"name": group_identifier, "users": [], "permissions": []} for username in group_config["users"]: - group_info['users'].append(username) + group_info["users"].append(username) group_permissions_by_group[group_identifier] = group_info if "permissions" in permission_configs: for _permission_identifier, permission_config in permission_configs["permissions"].items(): uri = permission_config["uri"] for group_identifier in permission_config["groups"]: - group_permissions_by_group[group_identifier]['permissions'].append( - {'actions': permission_config["allowed_permissions"], "uri": uri} + group_permissions_by_group[group_identifier]["permissions"].append( + {"actions": permission_config["allowed_permissions"], "uri": uri} ) return list(group_permissions_by_group.values()) @classmethod - def add_permissions_from_group_permissions(cls, group_permissions: list[GroupPermissionsDict], user_model: Optional[UserModel] = None) -> DesiredPermissionDict: + def add_permissions_from_group_permissions( + cls, group_permissions: list[GroupPermissionsDict], user_model: Optional[UserModel] = None + ) -> DesiredPermissionDict: unique_user_group_identifiers: Set[str] = set() user_to_group_identifiers: list[UserToGroupDict] = [] permission_assignments = [] default_group = None - default_group_identifier = current_app.config['SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP'] + default_group_identifier = current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"] if default_group_identifier: default_group = GroupService.find_or_create_group(default_group_identifier) unique_user_group_identifiers.add(default_group_identifier) 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 2cd16063..54d67848 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -2349,7 +2349,6 @@ class TestProcessApi(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_correct_user_can_get_and_update_a_task.""" initiator_user = self.find_or_create_user("testuser4") finance_user = self.find_or_create_user("testuser2") assert initiator_user.principal is not None @@ -2372,15 +2371,8 @@ class TestProcessApi(BaseTest): bpmn_file_location=bpmn_file_location, ) - # process_model = load_test_spec( - # process_model_id="model_with_lanes", - # bpmn_file_name="lanes.bpmn", - # process_group_id="finance", - # ) - response = self.create_process_instance_from_process_model_id_with_api( client, - # process_model.process_group_id, process_model_identifier, headers=self.logged_in_headers(initiator_user), ) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 380677ae..a1f41d3e 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -6,8 +6,8 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.models.user import UserNotFoundError -from spiffworkflow_backend.services.authorization_service import AuthorizationService, GroupPermissionsDict +from spiffworkflow_backend.services.authorization_service import AuthorizationService +from spiffworkflow_backend.services.authorization_service import GroupPermissionsDict from spiffworkflow_backend.services.authorization_service import InvalidPermissionError from spiffworkflow_backend.services.group_service import GroupService from spiffworkflow_backend.services.process_instance_processor import ( @@ -47,13 +47,13 @@ class TestAuthorizationService(BaseTest): assert testuser1_group_identifiers == ["Finance Team", "everybody"] assert len(users["testuser2"].groups) == 3 - self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance/model1") - self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance/") + self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance:model1") + self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/finance") self.assert_user_has_permission(users["testuser1"], "update", "/v1.0/process-groups/", expected_result=False) - self.assert_user_has_permission(users["testuser4"], "read", "/v1.0/process-groups/finance/model1") - self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/finance/model1") - self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/", expected_result=False) - self.assert_user_has_permission(users["testuser2"], "read", "/v1.0/process-groups/") + self.assert_user_has_permission(users["testuser4"], "read", "/v1.0/process-groups/finance:model1") + self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups/finance:model1") + self.assert_user_has_permission(users["testuser2"], "update", "/v1.0/process-groups", expected_result=False) + self.assert_user_has_permission(users["testuser2"], "read", "/v1.0/process-groups") def test_user_can_be_added_to_human_task_on_first_login( self, @@ -110,7 +110,6 @@ class TestAuthorizationService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - """Test_explode_permissions_all_on_process_group.""" expected_permissions = sorted( [ ("/event-error-details/some-process-group:some-process-model:*", "read"), From a445badcd16a85ab6abc7fe2a1944206b957ecc7 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 12:35:23 -0400 Subject: [PATCH 10/60] moved remove permission code to own method and some cleanup --- .../services/authorization_service.py | 78 +++++++++---------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 32efe6d8..1ff58394 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -1,4 +1,3 @@ -"""Authorization_service.""" import inspect import re from dataclasses import dataclass @@ -40,25 +39,23 @@ from spiffworkflow_backend.services.user_service import UserService class PermissionsFileNotSetError(Exception): - """PermissionsFileNotSetError.""" + pass class HumanTaskNotFoundError(Exception): - """HumanTaskNotFoundError.""" + pass class UserDoesNotHaveAccessToTaskError(Exception): - """UserDoesNotHaveAccessToTaskError.""" + pass class InvalidPermissionError(Exception): - """InvalidPermissionError.""" + pass @dataclass class PermissionToAssign: - """PermissionToAssign.""" - permission: str target_uri: str @@ -91,7 +88,7 @@ class UserToGroupDict(TypedDict): group_identifier: str -class DesiredPermissionDict(TypedDict): +class AddedPermissionDict(TypedDict): group_identifiers: Set[str] permission_assignments: list[PermissionAssignmentModel] user_to_group_identifiers: list[UserToGroupDict] @@ -114,7 +111,6 @@ class AuthorizationService: # https://stackoverflow.com/a/71320673/6090676 @classmethod def verify_sha256_token(cls, auth_header: Optional[str]) -> None: - """Verify_sha256_token.""" if auth_header is None: raise TokenNotProvidedError( "unauthorized", @@ -130,7 +126,6 @@ class AuthorizationService: @classmethod def has_permission(cls, principals: list[PrincipalModel], permission: str, target_uri: str) -> bool: - """Has_permission.""" principal_ids = [p.id for p in principals] target_uri_normalized = target_uri.removeprefix(V1_API_PATH_PREFIX) @@ -160,7 +155,6 @@ class AuthorizationService: @classmethod def user_has_permission(cls, user: UserModel, permission: str, target_uri: str) -> bool: - """User_has_permission.""" if user.principal is None: raise MissingPrincipalError(f"Missing principal for user with id: {user.id}") @@ -186,7 +180,6 @@ class AuthorizationService: @classmethod def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None: - """Associate_user_with_group.""" user_group_assignemnt = UserGroupAssignmentModel.query.filter_by(user_id=user.id, group_id=group.id).first() if user_group_assignemnt is None: user_group_assignemnt = UserGroupAssignmentModel(user_id=user.id, group_id=group.id) @@ -194,7 +187,7 @@ class AuthorizationService: db.session.commit() @classmethod - def import_permissions_from_yaml_file(cls, user_model: Optional[UserModel] = None) -> DesiredPermissionDict: + def import_permissions_from_yaml_file(cls, user_model: Optional[UserModel] = None) -> AddedPermissionDict: group_permissions = cls.parse_permissions_yaml_into_group_info() result = cls.add_permissions_from_group_permissions(group_permissions, user_model) return result @@ -292,7 +285,6 @@ class AuthorizationService: @classmethod def check_for_permission(cls) -> None: - """Check_for_permission.""" if cls.should_disable_auth_for_request(): return None @@ -326,11 +318,6 @@ class AuthorizationService: @staticmethod def decode_auth_token(auth_token: str) -> dict[str, Union[str, None]]: - """Decode the auth token. - - :param auth_token: - :return: integer|string - """ secret_key = current_app.config.get("SECRET_KEY") if secret_key is None: raise KeyError("we need current_app.config to have a SECRET_KEY") @@ -374,10 +361,11 @@ class AuthorizationService: @classmethod def create_user_from_sign_in(cls, user_info: dict) -> UserModel: - """Create_user_from_sign_in.""" - """Name, family_name, given_name, middle_name, nickname, preferred_username,""" - """Profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at. """ - """Email.""" + """Fields from user_info. + + name, family_name, given_name, middle_name, nickname, preferred_username, + profile, picture, website, gender, birthdate, zoneinfo, locale,updated_at, email. + """ is_new_user = False user_attributes = {} @@ -450,7 +438,6 @@ class AuthorizationService: process_related_path_segment: str, target_uris: list[str], ) -> list[PermissionToAssign]: - """Get_permissions_to_assign.""" permissions = permission_set.split(",") if permission_set == "all": permissions = ["create", "read", "update", "delete"] @@ -500,7 +487,6 @@ class AuthorizationService: @classmethod def set_basic_permissions(cls) -> list[PermissionToAssign]: - """Set_basic_permissions.""" permissions_to_assign: list[PermissionToAssign] = [] permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/process-instances/for-me")) permissions_to_assign.append( @@ -528,7 +514,6 @@ class AuthorizationService: @classmethod def set_process_group_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]: - """Set_process_group_permissions.""" permissions_to_assign: list[PermissionToAssign] = [] process_group_identifier = target.removeprefix("PG:").replace("/", ":").removeprefix(":") process_related_path_segment = f"{process_group_identifier}:*" @@ -545,7 +530,6 @@ class AuthorizationService: @classmethod def set_process_model_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]: - """Set_process_model_permissions.""" permissions_to_assign: list[PermissionToAssign] = [] process_model_identifier = target.removeprefix("PM:").replace("/", ":").removeprefix(":") process_related_path_segment = f"{process_model_identifier}/*" @@ -668,7 +652,7 @@ class AuthorizationService: @classmethod def add_permissions_from_group_permissions( cls, group_permissions: list[GroupPermissionsDict], user_model: Optional[UserModel] = None - ) -> DesiredPermissionDict: + ) -> AddedPermissionDict: unique_user_group_identifiers: Set[str] = set() user_to_group_identifiers: list[UserToGroupDict] = [] permission_assignments = [] @@ -715,19 +699,18 @@ class AuthorizationService: } @classmethod - def refresh_permissions(cls, group_permissions: list[GroupPermissionsDict]) -> None: - """Adds new permission assignments and deletes old ones.""" - initial_permission_assignments = PermissionAssignmentModel.query.all() - initial_user_to_group_assignments = UserGroupAssignmentModel.query.all() - group_permissions = group_permissions + cls.parse_permissions_yaml_into_group_info() - - result = cls.add_permissions_from_group_permissions(group_permissions) - desired_permission_assignments = result["permission_assignments"] - desired_group_identifiers = result["group_identifiers"] - desired_user_to_group_identifiers = result["user_to_group_identifiers"] + def remove_old_permissions_from_added_permissions( + cls, + added_permissions: AddedPermissionDict, + initial_permission_assignments: list[PermissionAssignmentModel], + initial_user_to_group_assignments: list[UserGroupAssignmentModel], + ) -> None: + added_permission_assignments = added_permissions["permission_assignments"] + added_group_identifiers = added_permissions["group_identifiers"] + added_user_to_group_identifiers = added_permissions["user_to_group_identifiers"] for ipa in initial_permission_assignments: - if ipa not in desired_permission_assignments: + if ipa not in added_permission_assignments: db.session.delete(ipa) for iutga in initial_user_to_group_assignments: @@ -740,12 +723,23 @@ class AuthorizationService: "username": iutga.user.username, "group_identifier": iutga.group.identifier, } - if current_user_dict not in desired_user_to_group_identifiers: + if current_user_dict not in added_user_to_group_identifiers: db.session.delete(iutga) # do not remove the default user group - desired_group_identifiers.add(current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]) - groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(desired_group_identifiers)).all() + added_group_identifiers.add(current_app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"]) + groups_to_delete = GroupModel.query.filter(GroupModel.identifier.not_in(added_group_identifiers)).all() for gtd in groups_to_delete: db.session.delete(gtd) db.session.commit() + + @classmethod + def refresh_permissions(cls, group_permissions: list[GroupPermissionsDict]) -> None: + """Adds new permission assignments and deletes old ones.""" + initial_permission_assignments = PermissionAssignmentModel.query.all() + initial_user_to_group_assignments = UserGroupAssignmentModel.query.all() + group_permissions = group_permissions + cls.parse_permissions_yaml_into_group_info() + added_permissions = cls.add_permissions_from_group_permissions(group_permissions) + cls.remove_old_permissions_from_added_permissions( + added_permissions, initial_permission_assignments, initial_user_to_group_assignments + ) From 0bd16283fce2763e29bf26e6d06160819542838f Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 15:11:30 -0400 Subject: [PATCH 11/60] allow prepending test case data with process id and added better error formatting w/ burnettk --- .../process_model_test_runner_service.py | 120 +++++++++++++----- .../basic_call_activity.bpmn | 1 - .../test_basic_manual_task.json | 2 +- .../basic_service_task/process_model.json | 2 +- .../test_basic_service_task.json | 4 +- .../unit/test_process_model_test_runner.py | 27 ++-- 6 files changed, 104 insertions(+), 52 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 7a5569e6..1e0ea82a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -13,7 +13,6 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState from spiffworkflow_backend.services.custom_parser import MyCustomParser -from spiffworkflow_backend.services.spec_file_service import SpecFileService # workflow json for test case @@ -45,17 +44,24 @@ class TestCaseResult: passed: bool bpmn_file: str test_case_name: str - error: Optional[str] = None + error_messages: Optional[list[str]] = None DEFAULT_NSMAP = { - 'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL', - 'bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI', - 'dc': 'http://www.omg.org/spec/DD/20100524/DC', + "bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL", + "bpmndi": "http://www.omg.org/spec/BPMN/20100524/DI", + "dc": "http://www.omg.org/spec/DD/20100524/DC", } # input: +# BPMN_TASK_IDENTIIFER: +# can be either task bpmn identifier or in format: +# [BPMN_PROCESS_ID]:[TASK_BPMN_IDENTIFIER] +# example: 'BasicServiceTaskProcess:service_task_one' +# this allows for tasks to share bpmn identifiers across models +# which is useful for call activities +# # json_file: # { # [TEST_CASE_NAME]: { @@ -78,15 +84,20 @@ class ProcessModelTestRunner: process_model_directory_path: str, process_model_directory_for_test_discovery: Optional[str] = None, instantiate_executer_callback: Optional[Callable[[str], Any]] = None, - execute_task_callback: Optional[Callable[[Any, Optional[dict]], Any]] = None, + execute_task_callback: Optional[Callable[[Any, str, Optional[dict]], Any]] = None, get_next_task_callback: Optional[Callable[[Any], Any]] = None, ) -> None: self.process_model_directory_path = process_model_directory_path - self.process_model_directory_for_test_discovery = process_model_directory_for_test_discovery or process_model_directory_path + self.process_model_directory_for_test_discovery = ( + process_model_directory_for_test_discovery or process_model_directory_path + ) self.instantiate_executer_callback = instantiate_executer_callback self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback + # keep track of the current task data index + self.task_data_index: dict[str, int] = {} + self.test_case_results: list[TestCaseResult] = [] self.bpmn_processes_to_file_mappings: dict[str, str] = {} self.bpmn_files_to_called_element_mappings: dict[str, list[str]] = {} @@ -95,12 +106,28 @@ class ProcessModelTestRunner: self._discover_process_model_processes() def all_test_cases_passed(self) -> bool: - failed_tests = [t for t in self.test_case_results if t.passed is False] + failed_tests = self.failing_tests() return len(failed_tests) < 1 + def failing_tests(self) -> list[TestCaseResult]: + return [t for t in self.test_case_results if t.passed is False] + + def failing_tests_formatted(self) -> str: + formatted_tests = ["FAILING TESTS:"] + for failing_test in self.failing_tests(): + msg = '' + if failing_test.error_messages: + msg = '\n\t\t'.join(failing_test.error_messages) + formatted_tests.append( + f'\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}' + ) + return '\n'.join(formatted_tests) + def run(self) -> None: if len(self.test_mappings.items()) < 1: - raise NoTestCasesFoundError(f"Could not find any test cases in given directory: {self.process_model_directory_for_test_discovery}") + raise NoTestCasesFoundError( + f"Could not find any test cases in given directory: {self.process_model_directory_for_test_discovery}" + ) for json_test_case_file, bpmn_file in self.test_mappings.items(): with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) @@ -109,16 +136,21 @@ class ProcessModelTestRunner: try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - self._add_test_result(False, bpmn_file, test_case_name, f"Syntax error: {str(ex)}") + ex_as_array = str(ex).split('\n') + self._add_test_result(False, bpmn_file, test_case_name, ex_as_array) def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: bpmn_process_instance = self._instantiate_executer(bpmn_file) next_task = self._get_next_task(bpmn_process_instance) while next_task is not None: test_case_task_properties = None + test_case_task_key = next_task.task_spec.bpmn_id if "tasks" in test_case_contents: - if next_task.task_spec.bpmn_id in test_case_contents["tasks"]: - test_case_task_properties = test_case_contents["tasks"][next_task.task_spec.bpmn_id] + if test_case_task_key not in test_case_contents["tasks"]: + # we may need to go to the top level workflow of a given bpmn file + test_case_task_key = f"{next_task.workflow.spec.name}:{next_task.task_spec.bpmn_id}" + if test_case_task_key in test_case_contents["tasks"]: + test_case_task_properties = test_case_contents["tasks"][test_case_task_key] task_type = next_task.task_spec.__class__.__name__ if task_type in ["ServiceTask", "UserTask", "CallActivity"] and test_case_task_properties is None: @@ -126,17 +158,29 @@ class ProcessModelTestRunner: f"Cannot run test case '{test_case_name}'. It requires task data for" f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) - self._execute_task(next_task, test_case_task_properties) + self._execute_task(next_task, test_case_task_key, test_case_task_properties) next_task = self._get_next_task(bpmn_process_instance) - test_passed = test_case_contents["expected_output_json"] == bpmn_process_instance.data + error_message = None - if test_passed is False: - error_message = ( - f"Expected output did not match actual output:" - f"\nexpected: {test_case_contents['expected_output_json']}" - f"\nactual: {bpmn_process_instance.data}" - ) - self._add_test_result(test_passed, bpmn_file, test_case_name, error_message) + if bpmn_process_instance.is_completed() is False: + error_message = [ + "Expected process instance to complete but it did not.", + f"Final data was: {bpmn_process_instance.last_task.data}", + f"Last task bpmn id: {bpmn_process_instance.last_task.task_spec.bpmn_id}", + f"Last task type: {bpmn_process_instance.last_task.task_spec.__class__.__name__}", + ] + elif bpmn_process_instance.success is False: + error_message = [ + "Expected process instance to succeed but it did not.", + f"Final data was: {bpmn_process_instance.data}", + ] + elif test_case_contents["expected_output_json"] != bpmn_process_instance.data: + error_message = [ + "Expected output did not match actual output:", + f"expected: {test_case_contents['expected_output_json']}", + f"actual: {bpmn_process_instance.data}", + ] + self._add_test_result(error_message is None, bpmn_file, test_case_name, error_message) def _discover_process_model_test_cases( self, @@ -168,22 +212,22 @@ class ProcessModelTestRunner: file_norm = os.path.normpath(file) if file_norm not in self.bpmn_files_to_called_element_mappings: self.bpmn_files_to_called_element_mappings[file_norm] = [] - with open(file_norm, 'rb') as f: + with open(file_norm, "rb") as f: file_contents = f.read() etree_xml_parser = etree.XMLParser(resolve_entities=False) root = etree.fromstring(file_contents, parser=etree_xml_parser) - call_activities = root.findall('.//bpmn:callActivity', namespaces=DEFAULT_NSMAP) + call_activities = root.findall(".//bpmn:callActivity", namespaces=DEFAULT_NSMAP) for call_activity in call_activities: - called_element = call_activity.attrib['calledElement'] + called_element = call_activity.attrib["calledElement"] self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) - bpmn_process_identifier = bpmn_process_element.attrib['id'] + bpmn_process_identifier = bpmn_process_element.attrib["id"] self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm - def _execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: + def _execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: if self.execute_task_callback: - self.execute_task_callback(spiff_task, test_case_task_properties) - self._default_execute_task(spiff_task, test_case_task_properties) + self.execute_task_callback(spiff_task, test_case_task_key, test_case_task_properties) + self._default_execute_task(spiff_task, test_case_task_key, test_case_task_properties) def _get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: if self.get_next_task_callback: @@ -201,10 +245,13 @@ class ProcessModelTestRunner: return ready_tasks[0] return None - def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: - if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == 'ServiceTask': - if test_case_task_properties and 'data' in test_case_task_properties: - spiff_task.update_data(test_case_task_properties['data']) + def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: + if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == "ServiceTask": + if test_case_task_properties and "data" in test_case_task_properties: + if test_case_task_key not in self.task_data_index: + self.task_data_index[test_case_task_key] = 0 + spiff_task.update_data(test_case_task_properties["data"][self.task_data_index[test_case_task_key]]) + self.task_data_index[test_case_task_key] += 1 spiff_task.complete() else: spiff_task.run() @@ -247,13 +294,16 @@ class ProcessModelTestRunner: bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) return bpmn_process_instance - def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error: Optional[str] = None) -> None: - bpmn_file_relative = os.path.relpath(bpmn_file, start=self.process_model_directory_path) + def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: + return os.path.relpath(bpmn_file, start=self.process_model_directory_path) + + def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None) -> None: + bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file) test_result = TestCaseResult( passed=passed, bpmn_file=bpmn_file_relative, test_case_name=test_case_name, - error=error, + error_messages=error_messages, ) self.test_case_results.append(test_result) diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn index ab796146..f837163f 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn @@ -10,7 +10,6 @@ - Flow_0ext5lt Flow_1hzwssi diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json index d82f5c7c..fab44ab7 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json @@ -2,7 +2,7 @@ "test_case_one": { "tasks": { "manual_task_one": { - "data": {} + "data": [{}] } }, "expected_output_json": {} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json index 0946afc6..b5e63674 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json @@ -7,4 +7,4 @@ "files": [], "primary_file_name": "A.1.0.2.bpmn", "primary_process_id": "Process_test_a102_A_1_0_2_bd2e724" -} \ No newline at end of file +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json index 729d81ac..da0b47a1 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json @@ -1,8 +1,8 @@ { "test_case_one": { "tasks": { - "service_task_one": { - "data": { "the_result": "result_from_service" } + "BasicServiceTaskProcess:service_task_one": { + "data": [{ "the_result": "result_from_service" }] } }, "expected_output_json": { "the_result": "result_from_service" } diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index a66c9f8a..80db5191 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -10,7 +10,8 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError, ProcessModelTestRunner +from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner class TestProcessModelTestRunner(BaseTest): @@ -20,7 +21,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests('basic_script_task') + process_model_test_runner = self._run_model_tests("basic_script_task") assert len(process_model_test_runner.test_case_results) == 1 def test_will_raise_if_no_tests_found( @@ -29,9 +30,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = ProcessModelTestRunner( - os.path.join(FileSystemService.root_path(), "DNE") - ) + process_model_test_runner = ProcessModelTestRunner(os.path.join(FileSystemService.root_path(), "DNE")) with pytest.raises(NoTestCasesFoundError): process_model_test_runner.run() assert process_model_test_runner.all_test_cases_passed(), process_model_test_runner.test_case_results @@ -51,7 +50,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(parent_directory='failing_tests') + process_model_test_runner = self._run_model_tests(parent_directory="failing_tests") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_call_activity( @@ -60,7 +59,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_call_activity') + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_call_activity") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_with_service_task( @@ -69,22 +68,26 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_service_task') + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_service_task") assert len(process_model_test_runner.test_case_results) == 1 - def _run_model_tests(self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = 'passing_tests') -> ProcessModelTestRunner: + def _run_model_tests( + self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "passing_tests" + ) -> ProcessModelTestRunner: base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments if bpmn_process_directory_name: path_segments = path_segments + [bpmn_process_directory_name] process_model_test_runner = ProcessModelTestRunner( process_model_directory_path=os.path.join(*base_process_model_dir_path_segments), - process_model_directory_for_test_discovery=os.path.join(*path_segments) + process_model_directory_for_test_discovery=os.path.join(*path_segments), ) process_model_test_runner.run() - all_tests_expected_to_pass = parent_directory == 'passing_tests' - assert process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass, process_model_test_runner.test_case_results + all_tests_expected_to_pass = parent_directory == "passing_tests" + assert ( + process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass + ), process_model_test_runner.failing_tests_formatted() return process_model_test_runner @pytest.fixture() From de24d76c9de8328917d3a39f111e181846bafc78 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 17:16:58 -0400 Subject: [PATCH 12/60] cleaned up runner tests and rsyncd back to sample w/ burnettk --- spiffworkflow-backend/pyproject.toml | 2 +- .../process_model_test_runner_service.py | 38 ++++-- .../failing_script_task.bpmn} | 0 .../failing_script_task}/process_model.json | 0 .../test_failing_script_task.json | 3 + .../call-activity/call_activity.bpmn} | 4 +- .../call-activity/process_model.json | 9 ++ .../call-activity/test_call_activity.json} | 2 +- .../choose-your-branch-schema.json | 11 ++ .../choose-your-branch-uischema.json | 5 + .../exclusive_gateway_based_on_user_task.bpmn | 98 ++++++++++++++++ .../process_model.json | 11 ++ ..._exclusive_gateway_based_on_user_task.json | 22 ++++ .../loopback_to_user_task.bpmn | 110 ++++++++++++++++++ .../loopback-to-user-task/process_model.json | 11 ++ .../test_loopback_to_user_task.json | 13 +++ .../user-input-schema.json | 11 ++ .../user-input-uischema.json | 5 + .../expected-to-pass/loopback/loopback.bpmn | 92 +++++++++++++++ .../loopback/process_model.json | 11 ++ .../loopback/test_loopback.json | 5 + .../manual-task/manual_task.bpmn} | 4 +- .../manual-task/process_model.json | 9 ++ .../manual-task/test_manual_task.json} | 2 +- .../expected-to-pass/process_group.json | 9 ++ .../script-task}/process_model.json | 0 .../script-task/script_task.bpmn} | 0 .../script-task/test_script_task.json} | 2 +- .../service-task}/process_model.json | 0 .../service-task/service_task.bpmn} | 10 +- .../service-task/test_service_task.json} | 4 +- .../test_basic_failing_script_task.json | 3 - .../basic_call_activity/process_model.json | 9 -- .../basic_manual_task/process_model.json | 9 -- .../unit/test_process_model_test_runner.py | 21 +++- 35 files changed, 492 insertions(+), 53 deletions(-) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn => expected-to-fail/failing_script_task/failing_script_task.bpmn} (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{failing_tests/basic_failing_script_task => expected-to-fail/failing_script_task}/process_model.json (100%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_call_activity/basic_call_activity.bpmn => expected-to-pass/call-activity/call_activity.bpmn} (95%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_call_activity/test_basic_call_activity.json => expected-to-pass/call-activity/test_call_activity.json} (65%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_manual_task/basic_manual_task.bpmn => expected-to-pass/manual-task/manual_task.bpmn} (93%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_manual_task/test_basic_manual_task.json => expected-to-pass/manual-task/test_manual_task.json} (84%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_script_task => expected-to-pass/script-task}/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_script_task/basic_script_task.bpmn => expected-to-pass/script-task/script_task.bpmn} (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_script_task/test_basic_script_task.json => expected-to-pass/script-task/test_script_task.json} (69%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_service_task => expected-to-pass/service-task}/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_service_task/basic_service_task.bpmn => expected-to-pass/service-task/service_task.bpmn} (89%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_service_task/test_basic_service_task.json => expected-to-pass/service-task/test_service_task.json} (69%) delete mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json delete mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json delete mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index d0f9edfe..39c81222 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -115,7 +115,7 @@ sphinx-click = "^4.3.0" Pygments = "^2.10.0" pyupgrade = "^3.1.0" furo = ">=2021.11.12" -myst-parser = "^0.15.1" +# myst-parser = "^0.15.1" [tool.poetry.scripts] spiffworkflow-backend = "spiffworkflow_backend.__main__:main" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 1e0ea82a..47875e10 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -39,6 +39,10 @@ class NoTestCasesFoundError(Exception): pass +class MissingInputTaskData(Exception): + pass + + @dataclass class TestCaseResult: passed: bool @@ -115,13 +119,11 @@ class ProcessModelTestRunner: def failing_tests_formatted(self) -> str: formatted_tests = ["FAILING TESTS:"] for failing_test in self.failing_tests(): - msg = '' + msg = "" if failing_test.error_messages: - msg = '\n\t\t'.join(failing_test.error_messages) - formatted_tests.append( - f'\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}' - ) - return '\n'.join(formatted_tests) + msg = "\n\t\t".join(failing_test.error_messages) + formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}") + return "\n".join(formatted_tests) def run(self) -> None: if len(self.test_mappings.items()) < 1: @@ -133,10 +135,11 @@ class ProcessModelTestRunner: json_file_contents = json.loads(f.read()) for test_case_name, test_case_contents in json_file_contents.items(): + self.task_data_index = {} try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - ex_as_array = str(ex).split('\n') + ex_as_array = str(ex).split("\n") self._add_test_result(False, bpmn_file, test_case_name, ex_as_array) def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: @@ -224,7 +227,9 @@ class ProcessModelTestRunner: bpmn_process_identifier = bpmn_process_element.attrib["id"] self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm - def _execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: + def _execute_task( + self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + ) -> None: if self.execute_task_callback: self.execute_task_callback(spiff_task, test_case_task_key, test_case_task_properties) self._default_execute_task(spiff_task, test_case_task_key, test_case_task_properties) @@ -245,12 +250,21 @@ class ProcessModelTestRunner: return ready_tasks[0] return None - def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: + def _default_execute_task( + self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + ) -> None: if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == "ServiceTask": if test_case_task_properties and "data" in test_case_task_properties: if test_case_task_key not in self.task_data_index: self.task_data_index[test_case_task_key] = 0 - spiff_task.update_data(test_case_task_properties["data"][self.task_data_index[test_case_task_key]]) + task_data_length = len(test_case_task_properties["data"]) + test_case_index = self.task_data_index[test_case_task_key] + if task_data_length <= test_case_index: + raise MissingInputTaskData( + f"Missing input task data for task: {test_case_task_key}. " + f"Only {task_data_length} given in the json but task was called {test_case_index + 1} times" + ) + spiff_task.update_data(test_case_task_properties["data"][test_case_index]) self.task_data_index[test_case_task_key] += 1 spiff_task.complete() else: @@ -297,7 +311,9 @@ class ProcessModelTestRunner: def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) - def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None) -> None: + def _add_test_result( + self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None + ) -> None: bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file) test_result = TestCaseResult( passed=passed, diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/failing_script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/failing_script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json new file mode 100644 index 00000000..0c81e072 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json @@ -0,0 +1,3 @@ +{ + "test_case_2": {} +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/call_activity.bpmn similarity index 95% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/call_activity.bpmn index f837163f..68a05f28 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/call_activity.bpmn @@ -1,6 +1,6 @@ - + Flow_0ext5lt @@ -9,7 +9,7 @@ Flow_1hzwssi - + Flow_0ext5lt Flow_1hzwssi diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json new file mode 100644 index 00000000..302fa24a --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "", + "display_name": "Call Activity", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "call_activity.bpmn", + "primary_process_id": "CallActivityProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/test_call_activity.json similarity index 65% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/test_call_activity.json index 0a7f7692..60e638a9 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/test_call_activity.json @@ -1,5 +1,5 @@ { - "test_case_one": { + "test_case_1": { "expected_output_json": {} } } diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json new file mode 100644 index 00000000..0bfee44c --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json @@ -0,0 +1,11 @@ +{ + "title": "Choose Your Branch", + "description": "", + "properties": { + "branch": { + "type": "string", + "title": "branch" + } + }, + "required": [] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json new file mode 100644 index 00000000..42449285 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json @@ -0,0 +1,5 @@ +{ + "ui:order": [ + "branch" + ] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn new file mode 100644 index 00000000..63bafd46 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn @@ -0,0 +1,98 @@ + + + + + Flow_19j3jcx + + + + Flow_0qa66xz + Flow_1ww41l3 + Flow_10m4g0q + + + + branch == 'a' + + + + Flow_1oxbb75 + Flow_1ck9lfk + + + + + + + + + + + Flow_19j3jcx + Flow_0qa66xz + + + Flow_1ww41l3 + Flow_1ck9lfk + chosen_branch = 'A' + + + Flow_10m4g0q + Flow_1oxbb75 + chosen_branch = 'B' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json new file mode 100644 index 00000000..44810510 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Exclusive Gateway Based on User Task", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "exclusive_gateway_based_on_user_task.bpmn", + "primary_process_id": "exclusive_gateway_based_on_user_task_process" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json new file mode 100644 index 00000000..e5d86ba0 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json @@ -0,0 +1,22 @@ +{ + "test_case_one": { + "tasks": { + "user_task_choose_branch": { + "data": [ + { "branch": "a" } + ] + } + }, + "expected_output_json": { "branch": "a", "chosen_branch": "A"} + }, + "test_case_two": { + "tasks": { + "user_task_choose_branch": { + "data": [ + { "branch": "b" } + ] + } + }, + "expected_output_json": { "branch": "b", "chosen_branch": "B"} + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn new file mode 100644 index 00000000..0298a1d6 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn @@ -0,0 +1,110 @@ + + + + + Flow_12xxe7w + + + + Flow_1s3znr2 + Flow_0utss6p + Flow_1sg0c65 + + + + Flow_0utss6p + + + counter == 3 + + + Flow_12xxe7w + Flow_08tc3r7 + counter = 1 +the_var = 0 + + + + + + + + + + + + Flow_08tc3r7 + Flow_1sg0c65 + Flow_0wnc5ju + + + + Flow_0wnc5ju + Flow_1s3znr2 + the_var = user_input_variable + the_var +counter += 1 + + + loop back if a < 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json new file mode 100644 index 00000000..06e8d935 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Loopback to User Task", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "loopback_to_user_task.bpmn", + "primary_process_id": "loopback_to_user_task_process" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json new file mode 100644 index 00000000..b78fc373 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json @@ -0,0 +1,13 @@ +{ + "test_case_one": { + "tasks": { + "user_task_enter_increment": { + "data": [ + { "user_input_variable": 7 }, + { "user_input_variable": 8 } + ] + } + }, + "expected_output_json": { "the_var": 15, "counter": 3, "user_input_variable": 8 } + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json new file mode 100644 index 00000000..b85521a4 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json @@ -0,0 +1,11 @@ +{ + "title": "User Input", + "description": "", + "properties": { + "user_input_variable": { + "type": "integer", + "title": "user_input_variable" + } + }, + "required": [] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json new file mode 100644 index 00000000..5015ba50 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json @@ -0,0 +1,5 @@ +{ + "ui:order": [ + "user_input_variable" + ] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn new file mode 100644 index 00000000..0d742ca7 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn @@ -0,0 +1,92 @@ + + + + + Flow_12xxe7w + + + + Flow_0wnc5ju + Flow_0utss6p + Flow_1sg0c65 + + + + Flow_0utss6p + + + a == 3 + + + Flow_12xxe7w + Flow_08tc3r7 + a = 1 + + + + + + + Flow_08tc3r7 + Flow_1sg0c65 + Flow_0wnc5ju + a += 1 + + + + loop back if a < 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json new file mode 100644 index 00000000..389bfeb4 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Loopback", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "loopback.bpmn", + "primary_process_id": "loopback_process" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json new file mode 100644 index 00000000..33655137 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json @@ -0,0 +1,5 @@ +{ + "test_case_1": { + "expected_output_json": { "a": 3 } + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/manual_task.bpmn similarity index 93% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/manual_task.bpmn index 5d0bf395..3ee85d4f 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/manual_task.bpmn @@ -1,6 +1,6 @@ - + Flow_0gz6i84 @@ -15,7 +15,7 @@ - + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json new file mode 100644 index 00000000..f843745d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "Manual Task", + "display_name": "Manual Task", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "manual_task.bpmn", + "primary_process_id": "ManualTaskProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/test_manual_task.json similarity index 84% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/test_manual_task.json index fab44ab7..889af937 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/test_manual_task.json @@ -1,5 +1,5 @@ { - "test_case_one": { + "test_case_1": { "tasks": { "manual_task_one": { "data": [{}] diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json new file mode 100644 index 00000000..9f562085 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json @@ -0,0 +1,9 @@ +{ + "admin": false, + "description": "", + "display_name": "Expected To Pass", + "display_order": 0, + "parent_groups": null, + "process_groups": [], + "process_models": [] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/test_script_task.json similarity index 69% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/test_script_task.json index 8eb2df13..98ff465b 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/test_script_task.json @@ -1,5 +1,5 @@ { - "test_case_one": { + "test_case_1": { "expected_output_json": { "a": 1 } } } diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/service_task.bpmn similarity index 89% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/service_task.bpmn index 8675d591..178c16da 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/service_task.bpmn @@ -1,6 +1,6 @@ - + Flow_19ephzh @@ -14,10 +14,10 @@ - + - - + + This is the Service Task Unit Test Screen. @@ -28,7 +28,7 @@ - + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/test_service_task.json similarity index 69% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/test_service_task.json index da0b47a1..7b5aae53 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/test_service_task.json @@ -1,7 +1,7 @@ { - "test_case_one": { + "test_case_1": { "tasks": { - "BasicServiceTaskProcess:service_task_one": { + "ServiceTaskProcess:service_task_one": { "data": [{ "the_result": "result_from_service" }] } }, diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json deleted file mode 100644 index 55360600..00000000 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "test_case_two": {} -} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json deleted file mode 100644 index 8f4e4a04..00000000 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "", - "display_name": "Basic Call Activity", - "exception_notification_addresses": [], - "fault_or_suspend_on_exception": "fault", - "files": [], - "primary_file_name": "basic_call_activity.bpmn", - "primary_process_id": "BasicCallActivityProcess" -} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json deleted file mode 100644 index 743dd104..00000000 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "Baisc Manual Task", - "display_name": "Baisc Manual Task", - "exception_notification_addresses": [], - "fault_or_suspend_on_exception": "fault", - "files": [], - "primary_file_name": "baisc_manual_task.bpmn", - "primary_process_id": "BasicManualTaskProcess" -} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index 80db5191..53abc61c 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -21,7 +21,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests("basic_script_task") + process_model_test_runner = self._run_model_tests("script-task") assert len(process_model_test_runner.test_case_results) == 1 def test_will_raise_if_no_tests_found( @@ -50,7 +50,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(parent_directory="failing_tests") + process_model_test_runner = self._run_model_tests(parent_directory="expected-to-fail") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_call_activity( @@ -59,7 +59,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_call_activity") + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="call-activity") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_with_service_task( @@ -68,11 +68,20 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_service_task") + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="service-task") + assert len(process_model_test_runner.test_case_results) == 1 + + def test_can_test_process_model_with_loopback_to_user_task( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="loopback-to-user-task") assert len(process_model_test_runner.test_case_results) == 1 def _run_model_tests( - self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "passing_tests" + self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "expected-to-pass" ) -> ProcessModelTestRunner: base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments @@ -84,7 +93,7 @@ class TestProcessModelTestRunner(BaseTest): ) process_model_test_runner.run() - all_tests_expected_to_pass = parent_directory == "passing_tests" + all_tests_expected_to_pass = parent_directory == "expected-to-pass" assert ( process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass ), process_model_test_runner.failing_tests_formatted() From 8d20ef69564e8005352c5d9b932178ce12c29ba8 Mon Sep 17 00:00:00 2001 From: burnettk Date: Thu, 18 May 2023 18:42:40 -0400 Subject: [PATCH 13/60] remove sphinx from pyproject since we do not use it in this way --- spiffworkflow-backend/poetry.lock | 4164 ++++++++++++-------------- spiffworkflow-backend/pyproject.toml | 10 +- 2 files changed, 1914 insertions(+), 2260 deletions(-) diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index e7e0ba37..407d0509 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -1,17 +1,3 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. - -[[package]] -name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, -] - [[package]] name = "alembic" version = "1.10.3" @@ -19,10 +5,6 @@ description = "A database migration tool for SQLAlchemy." category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, - {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, -] [package.dependencies] Mako = "*" @@ -39,10 +21,6 @@ description = "A library for parsing ISO 8601 strings." category = "main" optional = false python-versions = "*" -files = [ - {file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"}, - {file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"}, -] [package.extras] dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] @@ -54,10 +32,6 @@ description = "In-process task scheduler with Cron-like capabilities" category = "main" optional = false python-versions = ">=3.6" -files = [ - {file = "APScheduler-3.10.1-py3-none-any.whl", hash = "sha256:e813ad5ada7aff36fb08cdda746b520531eaac7757832abc204868ba78e0c8f6"}, - {file = "APScheduler-3.10.1.tar.gz", hash = "sha256:0293937d8f6051a0f493359440c1a1b93e882c57daf0197afeff0e727777b96e"}, -] [package.dependencies] pytz = "*" @@ -84,10 +58,6 @@ description = "An abstract syntax tree for Python with inference support." category = "main" optional = false python-versions = ">=3.7.2" -files = [ - {file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"}, - {file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"}, -] [package.dependencies] lazy-object-proxy = ">=1.4.0" @@ -104,10 +74,6 @@ description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] [package.extras] cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] @@ -116,18 +82,6 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope.interface"] tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] -[[package]] -name = "babel" -version = "2.12.1" -description = "Internationalization utilities" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, - {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, -] - [[package]] name = "bandit" version = "1.7.2" @@ -135,10 +89,6 @@ description = "Security oriented static analyser for python code." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "bandit-1.7.2-py3-none-any.whl", hash = "sha256:e20402cadfd126d85b68ed4c8862959663c8c372dbbb1fca8f8e2c9f55a067ec"}, - {file = "bandit-1.7.2.tar.gz", hash = "sha256:6d11adea0214a43813887bfe71a377b5a9955e4c826c8ffd341b494e3ab25260"}, -] [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} @@ -158,53 +108,11 @@ description = "Modern password hashing for your software and your servers" category = "main" optional = false python-versions = ">=3.6" -files = [ - {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, - {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, - {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, - {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, -] [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] -[[package]] -name = "beautifulsoup4" -version = "4.12.2" -description = "Screen-scraping library" -category = "dev" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - [[package]] name = "black" version = "22.12.0" @@ -212,20 +120,6 @@ description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] [package.dependencies] click = ">=8.0.0" @@ -248,10 +142,6 @@ description = "Fast, simple object-to-object and broadcast signaling" category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "blinker-1.6-py3-none-any.whl", hash = "sha256:eeebd5dfc782e1817fe4261ce79936c8c8cefb90d685caf50cec458029f773c1"}, - {file = "blinker-1.6.tar.gz", hash = "sha256:5874afe21df4bae8885d31a0a6c4b5861910a575eae6176f051fbb9a6571481b"}, -] [package.dependencies] typing-extensions = "*" @@ -263,10 +153,6 @@ description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" -files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] [[package]] name = "cffi" @@ -275,7 +161,1779 @@ description = "Foreign Function Interface for Python calling C code." category = "main" optional = false python-versions = "*" -files = [ + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "charset-normalizer" +version = "3.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" + +[[package]] +name = "classify-imports" +version = "4.2.0" +description = "Utilities for refactoring imports in python-like syntax." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "clickclick" +version = "20.10.2" +description = "Click utility functions" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=4.0" +PyYAML = ">=3.11" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "configparser" +version = "5.3.0" +description = "Updated configparser from stdlib for earlier Pythons." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "types-backports"] + +[[package]] +name = "connexion" +version = "2.14.2" +description = "Connexion - API first applications with OpenAPI/Swagger and Flask" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +clickclick = ">=1.2,<21" +flask = ">=1.0.4,<2.3" +inflection = ">=0.3.1,<0.6" +itsdangerous = ">=0.24" +jsonschema = ">=2.5.1,<5" +packaging = ">=20" +PyYAML = ">=5.1,<7" +requests = ">=2.9.1,<3" +swagger-ui-bundle = {version = ">=0.0.2,<0.1", optional = true, markers = "extra == \"swagger-ui\""} +werkzeug = ">=1.0,<2.3" + +[package.extras] +aiohttp = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)"] +docs = ["sphinx-autoapi (==1.8.1)"] +flask = ["flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)"] +swagger-ui = ["swagger-ui-bundle (>=0.0.2,<0.1)"] +tests = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)", "aiohttp-remotes", "decorator (>=5,<6)", "flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)", "pytest (>=6,<7)", "pytest-aiohttp", "pytest-cov (>=2,<3)", "swagger-ui-bundle (>=0.0.2,<0.1)", "testfixtures (>=6,<7)"] + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "39.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] + +[[package]] +name = "darglint" +version = "1.8.1" +description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "dateparser" +version = "1.1.8" +description = "Date parsing library designed to parse dates from HTML pages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +python-dateutil = "*" +pytz = "*" +regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27" +tzlocal = "*" + +[package.extras] +calendars = ["convertdate", "hijri-converter"] +fasttext = ["fasttext"] +langdetect = ["langdetect"] + +[[package]] +name = "dill" +version = "0.3.6" +description = "serialize all of python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "docutils" +version = "0.19" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "dparse" +version = "0.6.2" +description = "A parser for Python dependency files" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +packaging = "*" +toml = "*" + +[package.extras] +conda = ["pyyaml"] +pipenv = ["pipenv"] + +[[package]] +name = "exceptiongroup" +version = "1.1.1" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.11.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "flake8-bandit" +version = "2.1.2" +description = "Automated security testing with bandit and flake8." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bandit = "*" +flake8 = "*" +flake8-polyfill = "*" +pycodestyle = "*" + +[[package]] +name = "flake8-bugbear" +version = "22.12.6" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"] + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-rst-docstrings" +version = "0.2.7" +description = "Python docstring reStructuredText (RST) validator" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3.0.0" +pygments = "*" +restructuredtext-lint = "*" + +[[package]] +name = "flask" +version = "2.2.2" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=8.0" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.2.2" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-admin" +version = "1.6.1" +description = "Simple and extensible admin interface framework for Flask" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Flask = ">=0.7" +wtforms = "*" + +[package.extras] +aws = ["boto"] +azure = ["azure-storage-blob"] + +[[package]] +name = "flask-bcrypt" +version = "1.0.1" +description = "Brcrypt hashing for Flask." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.1" +Flask = "*" + +[[package]] +name = "flask-cors" +version = "3.0.10" +description = "A Flask extension adding a decorator for CORS support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Flask = ">=0.9" +Six = "*" + +[[package]] +name = "flask-jwt-extended" +version = "4.4.4" +description = "Extended JWT integration with Flask" +category = "main" +optional = false +python-versions = ">=3.7,<4" + +[package.dependencies] +Flask = ">=2.0,<3.0" +PyJWT = ">=2.0,<3.0" +Werkzeug = ">=0.14" + +[package.extras] +asymmetric-crypto = ["cryptography (>=3.3.1)"] + +[[package]] +name = "flask-mail" +version = "0.9.1" +description = "Flask extension for sending email" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +blinker = "*" +Flask = "*" + +[[package]] +name = "flask-marshmallow" +version = "0.15.0" +description = "Flask + marshmallow for beautiful APIs" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Flask = "*" +marshmallow = ">=3.0.0" +packaging = ">=17.0" + +[package.extras] +dev = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.28.2)", "mock", "pre-commit", "pytest", "tox"] +docs = ["Sphinx (==6.1.3)", "marshmallow-sqlalchemy (>=0.13.0)", "sphinx-issues (==3.0.1)"] +sqlalchemy = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.28.2)"] +tests = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.28.2)", "mock", "pytest"] + +[[package]] +name = "flask-migrate" +version = "4.0.4" +description = "SQLAlchemy database migrations for Flask applications using Alembic." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +alembic = ">=1.9.0" +Flask = ">=0.9" +Flask-SQLAlchemy = ">=1.0" + +[[package]] +name = "flask-restful" +version = "0.3.9" +description = "Simple framework for creating REST APIs" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aniso8601 = ">=0.82" +Flask = ">=0.8" +pytz = "*" +six = ">=1.3.0" + +[package.extras] +docs = ["sphinx"] + +[[package]] +name = "flask-simple-crypt" +version = "0.3.3" +description = "Flask extension based on simple-crypt that allows simple, secure encryption and decryption for Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Flask = "*" +pycryptodome = "*" + +[[package]] +name = "flask-sqlalchemy" +version = "3.0.3" +description = "Add SQLAlchemy support to your Flask application." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Flask = ">=2.2" +SQLAlchemy = ">=1.4.18" + +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.31" +description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "greenlet" +version = "2.0.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "identify" +version = "2.5.22" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "6.2.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "main" +optional = false +python-versions = ">=3.8.0" + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsonschema" +version = "4.17.3" +description = "An implementation of JSON Schema validation for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +description = "A fast and thorough lazy object proxy." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "lxml" +version = "4.9.2" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "mako" +version = "1.2.4" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "marshmallow" +version = "3.19.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-enum" +version = "1.5.1" +description = "Enum field for Marshmallow" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +marshmallow = ">=2.0.0" + +[[package]] +name = "marshmallow-sqlalchemy" +version = "0.29.0" +description = "SQLAlchemy integration with the marshmallow (de)serialization library" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +marshmallow = ">=3.0.0" +packaging = ">=21.3" +SQLAlchemy = ">=1.4.40,<3.0" + +[package.extras] +dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.2.13)", "pre-commit (==3.1.0)", "pytest", "pytest-lazy-fixture (>=0.6.2)", "tox"] +docs = ["alabaster (==0.7.13)", "sphinx (==6.1.3)", "sphinx-issues (==3.0.1)"] +lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.2.13)", "pre-commit (==3.1.0)"] +tests = ["pytest", "pytest-lazy-fixture (>=0.6.2)"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "1.2.0" +description = "Optional static typing for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "mysql-connector-python" +version = "8.0.32" +description = "MySQL driver written in Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +protobuf = ">=3.11.0,<=3.20.3" + +[package.extras] +compression = ["lz4 (>=2.1.6,<=3.1.3)", "zstandard (>=0.12.0,<=0.19.0)"] +dns-srv = ["dnspython (>=1.16.0,<=2.1.0)"] +gssapi = ["gssapi (>=1.6.9,<=1.8.2)"] + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pbr" +version = "5.11.1" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "pep8-naming" +version = "0.13.2" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3.9.1" + +[[package]] +name = "platformdirs" +version = "3.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pre-commit-hooks" +version = "4.4.0" +description = "Some out-of-the-box hooks for pre-commit." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +"ruamel.yaml" = ">=0.15" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "prometheus-client" +version = "0.16.0" +description = "Python client for the Prometheus monitoring system." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prometheus-flask-exporter" +version = "0.22.3" +description = "Prometheus metrics exporter for Flask" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +flask = "*" +prometheus-client = "*" + +[[package]] +name = "protobuf" +version = "3.20.3" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "psycopg2" +version = "2.9.6" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycryptodome" +version = "3.17" +description = "Cryptographic library for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pyjwt" +version = "2.6.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pylint" +version = "2.17.2" +description = "python code static checker" +category = "main" +optional = false +python-versions = ">=3.7.2" + +[package.dependencies] +astroid = ">=2.15.2,<=2.17.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, +] +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyrsistent" +version = "0.19.3" +description = "Persistent/Functional/Immutable data structures" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pytest" +version = "7.2.2" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-flask" +version = "1.2.0" +description = "A set of py.test fixtures to test Flask applications." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +Flask = "*" +pytest = ">=5.2" +Werkzeug = ">=0.7" + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "pytest-flask-sqlalchemy" +version = "1.1.0" +description = "A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Flask-SQLAlchemy = ">=2.3" +packaging = ">=14.1" +pytest = ">=3.2.1" +pytest-mock = ">=1.6.2" +SQLAlchemy = ">=1.2.2" + +[package.extras] +tests = ["psycopg2-binary", "pytest (>=6.0.1)", "pytest-postgresql (>=2.4.0,<4.0.0)"] + +[[package]] +name = "pytest-mock" +version = "3.10.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pytz-deprecation-shim" +version = "0.1.0.post0" +description = "Shims to make deprecation of pytz easier" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +tzdata = {version = "*", markers = "python_version >= \"3.6\""} + +[[package]] +name = "pyupgrade" +version = "3.3.1" +description = "A tool to automatically upgrade syntax for newer versions." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tokenize-rt = ">=3.2.0" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "regex" +version = "2023.3.23" +description = "Alternative regular expression module, to replace re." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "reorder-python-imports" +version = "3.9.0" +description = "Tool for reordering python imports" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +classify-imports = ">=4.1" + +[[package]] +name = "requests" +version = "2.28.2" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "restrictedpython" +version = "6.0" +description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment." +category = "main" +optional = false +python-versions = ">=3.6, <3.12" + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] +test = ["pytest", "pytest-mock"] + +[[package]] +name = "restructuredtext-lint" +version = "1.4.0" +description = "reStructuredText linter" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +docutils = ">=0.11,<1.0" + +[[package]] +name = "ruamel-yaml" +version = "0.17.21" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.7" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "safety" +version = "2.3.5" +description = "Checks installed dependencies for known vulnerabilities and licenses." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Click = ">=8.0.2" +dparse = ">=0.6.2" +packaging = ">=21.0,<22.0" +requests = "*" +"ruamel.yaml" = ">=0.17.21" +setuptools = ">=19.3" + +[package.extras] +github = ["jinja2 (>=3.1.0)", "pygithub (>=1.43.3)"] +gitlab = ["python-gitlab (>=1.3.0)"] + +[[package]] +name = "sentry-sdk" +version = "1.19.1" +description = "Python client for Sentry (https://sentry.io)" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +certifi = "*" +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +arq = ["arq (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)"] +grpcio = ["grpcio (>=1.21.1)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setuptools" +version = "65.7.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "simplejson" +version = "3.19.1" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +category = "main" +optional = false +python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "spiff-element-units" +version = "0.3.0" +description = "" +category = "main" +optional = false +python-versions = ">=3.9" + +[[package]] +name = "SpiffWorkflow" +version = "1.2.1" +description = "A workflow framework and BPMN/DMN Processor" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.dependencies] +configparser = "*" +lxml = "*" + +[package.source] +type = "git" +url = "https://github.com/sartography/SpiffWorkflow" +reference = "main" +resolved_reference = "07c153f2dbf95fbed55a9d3bcfbfcf4f521dfae8" + +[[package]] +name = "sqlalchemy" +version = "2.0.9" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.2.0" + +[package.extras] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=7)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy-stubs" +version = "0.4" +description = "SQLAlchemy stubs and mypy plugin" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.dependencies] +mypy = ">=0.790" +typing-extensions = ">=3.7.4" + +[package.source] +type = "git" +url = "https://github.com/burnettk/sqlalchemy-stubs.git" +reference = "scoped-session-delete" +resolved_reference = "d1176931684ce5b327539cc9567d4a1cd8ef1efd" + +[[package]] +name = "stevedore" +version = "5.0.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "swagger-ui-bundle" +version = "0.0.9" +description = "swagger_ui_bundle - swagger-ui files in a pip package" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Jinja2 = ">=2.0" + +[[package]] +name = "tokenize-rt" +version = "5.0.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tomlkit" +version = "0.11.7" +description = "Style preserving TOML library" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typeguard" +version = "3.0.2" +description = "Run-time type checker for Python" +category = "dev" +optional = false +python-versions = ">=3.7.4" + +[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 = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy (>=0.991)", "pytest (>=7)"] + +[[package]] +name = "types-click" +version = "7.1.8" +description = "Typing stubs for click" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "types-dateparser" +version = "1.1.4.9" +description = "Typing stubs for dateparser" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "types-flask" +version = "1.1.6" +description = "Typing stubs for Flask" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +types-click = "*" +types-Jinja2 = "*" +types-Werkzeug = "*" + +[[package]] +name = "types-jinja2" +version = "2.11.9" +description = "Typing stubs for Jinja2" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +types-MarkupSafe = "*" + +[[package]] +name = "types-markupsafe" +version = "1.1.10" +description = "Typing stubs for MarkupSafe" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "types-pytz" +version = "2022.7.1.2" +description = "Typing stubs for pytz" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "types-pyyaml" +version = "6.0.12.9" +description = "Typing stubs for PyYAML" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "types-requests" +version = "2.28.11.17" +description = "Typing stubs for requests" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +types-urllib3 = "<1.27" + +[[package]] +name = "types-urllib3" +version = "1.26.25.10" +description = "Typing stubs for urllib3" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "types-werkzeug" +version = "1.0.9" +description = "Typing stubs for Werkzeug" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" + +[[package]] +name = "tzlocal" +version = "4.3" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytz-deprecation-shim = "*" +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "urllib3" +version = "1.26.15" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.21.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<4" + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "werkzeug" +version = "2.2.3" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "wtforms" +version = "3.0.1" +description = "Form validation and rendering for Python web development." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = "*" + +[package.extras] +email = ["email-validator"] + +[[package]] +name = "xdoctest" +version = "1.1.1" +description = "A rewrite of the builtin doctest module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", optional = true, markers = "platform_system == \"Windows\" and extra == \"colors\""} +Pygments = {version = "*", optional = true, markers = "python_version >= \"3.5.0\" and extra == \"colors\""} +six = "*" + +[package.extras] +all = ["IPython", "IPython", "Pygments", "Pygments", "attrs", "codecov", "colorama", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert", "pyflakes", "pytest", "pytest", "pytest", "pytest-cov", "six", "tomli", "typing"] +all-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "codecov (==2.0.15)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "pyflakes (==2.2.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "six (==1.11.0)", "tomli (==0.2.0)", "typing (==3.7.4)"] +colors = ["Pygments", "Pygments", "colorama"] +jupyter = ["IPython", "IPython", "attrs", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert"] +optional = ["IPython", "IPython", "Pygments", "Pygments", "attrs", "colorama", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert", "pyflakes", "tomli"] +optional-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "pyflakes (==2.2.0)", "tomli (==0.2.0)"] +runtime-strict = ["six (==1.11.0)"] +tests = ["codecov", "pytest", "pytest", "pytest", "pytest-cov", "typing"] +tests-binary = ["cmake", "cmake", "ninja", "ninja", "pybind11", "pybind11", "scikit-build", "scikit-build"] +tests-binary-strict = ["cmake (==3.21.2)", "cmake (==3.25.0)", "ninja (==1.10.2)", "ninja (==1.11.1)", "pybind11 (==2.10.3)", "pybind11 (==2.7.1)", "scikit-build (==0.11.1)", "scikit-build (==0.16.1)"] +tests-strict = ["codecov (==2.0.15)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "typing (==3.7.4)"] + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.9,<3.12" +content-hash = "0b45b6c77237e13145782e4062d693ba2dac9f43c3dffc923809d3dd5e45137d" + +[metadata.files] +alembic = [ + {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, + {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, +] +aniso8601 = [ + {file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"}, + {file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"}, +] +apscheduler = [ + {file = "APScheduler-3.10.1-py3-none-any.whl", hash = "sha256:e813ad5ada7aff36fb08cdda746b520531eaac7757832abc204868ba78e0c8f6"}, + {file = "APScheduler-3.10.1.tar.gz", hash = "sha256:0293937d8f6051a0f493359440c1a1b93e882c57daf0197afeff0e727777b96e"}, +] +astroid = [ + {file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"}, + {file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"}, +] +attrs = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] +bandit = [ + {file = "bandit-1.7.2-py3-none-any.whl", hash = "sha256:e20402cadfd126d85b68ed4c8862959663c8c372dbbb1fca8f8e2c9f55a067ec"}, + {file = "bandit-1.7.2.tar.gz", hash = "sha256:6d11adea0214a43813887bfe71a377b5a9955e4c826c8ffd341b494e3ab25260"}, +] +bcrypt = [ + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, +] +black = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] +blinker = [ + {file = "blinker-1.6-py3-none-any.whl", hash = "sha256:eeebd5dfc782e1817fe4261ce79936c8c8cefb90d685caf50cec458029f773c1"}, + {file = "blinker-1.6.tar.gz", hash = "sha256:5874afe21df4bae8885d31a0a6c4b5861910a575eae6176f051fbb9a6571481b"}, +] +certifi = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] +cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, @@ -341,30 +1999,11 @@ files = [ {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, ] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "dev" -optional = false -python-versions = ">=3.6.1" -files = [ +cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] - -[[package]] -name = "charset-normalizer" -version = "3.1.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.7.0" -files = [ +charset-normalizer = [ {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, @@ -441,117 +2080,31 @@ files = [ {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, ] - -[[package]] -name = "classify-imports" -version = "4.2.0" -description = "Utilities for refactoring imports in python-like syntax." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +classify-imports = [ {file = "classify_imports-4.2.0-py2.py3-none-any.whl", hash = "sha256:dbbc264b70a470ed8c6c95976a11dfb8b7f63df44ed1af87328bbed2663f5161"}, {file = "classify_imports-4.2.0.tar.gz", hash = "sha256:7abfb7ea92149b29d046bd34573d247ba6e68cc28100c801eba4af17964fc40e"}, ] - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "clickclick" -version = "20.10.2" -description = "Click utility functions" -category = "main" -optional = false -python-versions = "*" -files = [ +clickclick = [ {file = "clickclick-20.10.2-py2.py3-none-any.whl", hash = "sha256:c8f33e6d9ec83f68416dd2136a7950125bd256ec39ccc9a85c6e280a16be2bb5"}, {file = "clickclick-20.10.2.tar.gz", hash = "sha256:4efb13e62353e34c5eef7ed6582c4920b418d7dedc86d819e22ee089ba01802c"}, ] - -[package.dependencies] -click = ">=4.0" -PyYAML = ">=3.11" - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ +colorama = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] - -[[package]] -name = "configparser" -version = "5.3.0" -description = "Updated configparser from stdlib for earlier Pythons." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +configparser = [ {file = "configparser-5.3.0-py3-none-any.whl", hash = "sha256:b065779fd93c6bf4cee42202fa4351b4bb842e96a3fb469440e484517a49b9fa"}, {file = "configparser-5.3.0.tar.gz", hash = "sha256:8be267824b541c09b08db124917f48ab525a6c3e837011f3130781a224c57090"}, ] - -[package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "types-backports"] - -[[package]] -name = "connexion" -version = "2.14.2" -description = "Connexion - API first applications with OpenAPI/Swagger and Flask" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +connexion = [ {file = "connexion-2.14.2-py2.py3-none-any.whl", hash = "sha256:a73b96a0e07b16979a42cde7c7e26afe8548099e352cf350f80c57185e0e0b36"}, {file = "connexion-2.14.2.tar.gz", hash = "sha256:dbc06f52ebeebcf045c9904d570f24377e8bbd5a6521caef15a06f634cf85646"}, ] - -[package.dependencies] -clickclick = ">=1.2,<21" -flask = ">=1.0.4,<2.3" -inflection = ">=0.3.1,<0.6" -itsdangerous = ">=0.24" -jsonschema = ">=2.5.1,<5" -packaging = ">=20" -PyYAML = ">=5.1,<7" -requests = ">=2.9.1,<3" -swagger-ui-bundle = {version = ">=0.0.2,<0.1", optional = true, markers = "extra == \"swagger-ui\""} -werkzeug = ">=1.0,<2.3" - -[package.extras] -aiohttp = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)"] -docs = ["sphinx-autoapi (==1.8.1)"] -flask = ["flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)"] -swagger-ui = ["swagger-ui-bundle (>=0.0.2,<0.1)"] -tests = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)", "aiohttp-remotes", "decorator (>=5,<6)", "flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)", "pytest (>=6,<7)", "pytest-aiohttp", "pytest-cov (>=2,<3)", "swagger-ui-bundle (>=0.0.2,<0.1)", "testfixtures (>=6,<7)"] - -[[package]] -name = "coverage" -version = "6.5.0" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +coverage = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, @@ -603,21 +2156,7 @@ files = [ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cryptography" -version = "39.0.2" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +cryptography = [ {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06"}, {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7"}, {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612"}, @@ -642,505 +2181,113 @@ files = [ {file = "cryptography-39.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3"}, {file = "cryptography-39.0.2.tar.gz", hash = "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f"}, ] - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] -sdist = ["setuptools-rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] -test-randomorder = ["pytest-randomly"] -tox = ["tox"] - -[[package]] -name = "darglint" -version = "1.8.1" -description = "A utility for ensuring Google-style docstrings stay up to date with the source code." -category = "dev" -optional = false -python-versions = ">=3.6,<4.0" -files = [ +darglint = [ {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, ] - -[[package]] -name = "dateparser" -version = "1.1.8" -description = "Date parsing library designed to parse dates from HTML pages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +dateparser = [ {file = "dateparser-1.1.8-py2.py3-none-any.whl", hash = "sha256:070b29b5bbf4b1ec2cd51c96ea040dc68a614de703910a91ad1abba18f9f379f"}, {file = "dateparser-1.1.8.tar.gz", hash = "sha256:86b8b7517efcc558f085a142cdb7620f0921543fcabdb538c8a4c4001d8178e3"}, ] - -[package.dependencies] -python-dateutil = "*" -pytz = "*" -regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27" -tzlocal = "*" - -[package.extras] -calendars = ["convertdate", "hijri-converter"] -fasttext = ["fasttext"] -langdetect = ["langdetect"] - -[[package]] -name = "dill" -version = "0.3.6" -description = "serialize all of python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +dill = [ {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, ] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" -files = [ +distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] - -[[package]] -name = "docutils" -version = "0.19" -description = "Docutils -- Python Documentation Utilities" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +docutils = [ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] - -[[package]] -name = "dparse" -version = "0.6.2" -description = "A parser for Python dependency files" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +dparse = [ {file = "dparse-0.6.2-py3-none-any.whl", hash = "sha256:8097076f1dd26c377f30d4745e6ec18fef42f3bf493933b842ac5bafad8c345f"}, {file = "dparse-0.6.2.tar.gz", hash = "sha256:d45255bda21f998bc7ddf2afd5e62505ba6134756ba2d42a84c56b0826614dfe"}, ] - -[package.dependencies] -packaging = "*" -toml = "*" - -[package.extras] -conda = ["pyyaml"] -pipenv = ["pipenv"] - -[[package]] -name = "exceptiongroup" -version = "1.1.1" -description = "Backport of PEP 654 (exception groups)" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +exceptiongroup = [ {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, ] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.11.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +filelock = [ {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"}, {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"}, ] - -[package.extras] -docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] - -[package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" - -[[package]] -name = "flake8-bandit" -version = "2.1.2" -description = "Automated security testing with bandit and flake8." -category = "dev" -optional = false -python-versions = "*" -files = [ +flake8-bandit = [ {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, ] - -[package.dependencies] -bandit = "*" -flake8 = "*" -flake8-polyfill = "*" -pycodestyle = "*" - -[[package]] -name = "flake8-bugbear" -version = "22.12.6" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +flake8-bugbear = [ {file = "flake8-bugbear-22.12.6.tar.gz", hash = "sha256:4cdb2c06e229971104443ae293e75e64c6107798229202fbe4f4091427a30ac0"}, {file = "flake8_bugbear-22.12.6-py3-none-any.whl", hash = "sha256:b69a510634f8a9c298dfda2b18a8036455e6b19ecac4fe582e4d7a0abfa50a30"}, ] - -[package.dependencies] -attrs = ">=19.2.0" -flake8 = ">=3.0.0" - -[package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"] - -[[package]] -name = "flake8-docstrings" -version = "1.7.0" -description = "Extension for flake8 which uses pydocstyle to check docstrings" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +flake8-docstrings = [ {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, ] - -[package.dependencies] -flake8 = ">=3" -pydocstyle = ">=2.1" - -[[package]] -name = "flake8-polyfill" -version = "1.0.2" -description = "Polyfill package for Flake8 plugins" -category = "dev" -optional = false -python-versions = "*" -files = [ +flake8-polyfill = [ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] - -[package.dependencies] -flake8 = "*" - -[[package]] -name = "flake8-rst-docstrings" -version = "0.2.7" -description = "Python docstring reStructuredText (RST) validator" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +flake8-rst-docstrings = [ {file = "flake8-rst-docstrings-0.2.7.tar.gz", hash = "sha256:2740067ab9237559dd45a3434d8c987792c7b259ca563621a3b95efe201f5382"}, {file = "flake8_rst_docstrings-0.2.7-py3-none-any.whl", hash = "sha256:5d56075dce360bcc9c6775bfe7cb431aa395de600ca7e8d40580a28d50b2a803"}, ] - -[package.dependencies] -flake8 = ">=3.0.0" -pygments = "*" -restructuredtext-lint = "*" - -[[package]] -name = "flask" -version = "2.2.2" -description = "A simple framework for building complex web applications." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +flask = [ {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, ] - -[package.dependencies] -click = ">=8.0" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.2.2" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - -[[package]] -name = "flask-admin" -version = "1.6.1" -description = "Simple and extensible admin interface framework for Flask" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +flask-admin = [ {file = "Flask-Admin-1.6.1.tar.gz", hash = "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369"}, {file = "Flask_Admin-1.6.1-py3-none-any.whl", hash = "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406"}, ] - -[package.dependencies] -Flask = ">=0.7" -wtforms = "*" - -[package.extras] -aws = ["boto"] -azure = ["azure-storage-blob"] - -[[package]] -name = "flask-bcrypt" -version = "1.0.1" -description = "Brcrypt hashing for Flask." -category = "main" -optional = false -python-versions = "*" -files = [ +flask-bcrypt = [ {file = "Flask-Bcrypt-1.0.1.tar.gz", hash = "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369"}, {file = "Flask_Bcrypt-1.0.1-py3-none-any.whl", hash = "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a"}, ] - -[package.dependencies] -bcrypt = ">=3.1.1" -Flask = "*" - -[[package]] -name = "flask-cors" -version = "3.0.10" -description = "A Flask extension adding a decorator for CORS support" -category = "main" -optional = false -python-versions = "*" -files = [ +flask-cors = [ {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, ] - -[package.dependencies] -Flask = ">=0.9" -Six = "*" - -[[package]] -name = "flask-jwt-extended" -version = "4.4.4" -description = "Extended JWT integration with Flask" -category = "main" -optional = false -python-versions = ">=3.7,<4" -files = [ +flask-jwt-extended = [ {file = "Flask-JWT-Extended-4.4.4.tar.gz", hash = "sha256:62b521d75494c290a646ae8acc77123721e4364790f1e64af0038d823961fbf0"}, {file = "Flask_JWT_Extended-4.4.4-py2.py3-none-any.whl", hash = "sha256:a85eebfa17c339a7260c4643475af444784ba6de5588adda67406f0a75599553"}, ] - -[package.dependencies] -Flask = ">=2.0,<3.0" -PyJWT = ">=2.0,<3.0" -Werkzeug = ">=0.14" - -[package.extras] -asymmetric-crypto = ["cryptography (>=3.3.1)"] - -[[package]] -name = "flask-mail" -version = "0.9.1" -description = "Flask extension for sending email" -category = "main" -optional = false -python-versions = "*" -files = [ +flask-mail = [ {file = "Flask-Mail-0.9.1.tar.gz", hash = "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"}, ] - -[package.dependencies] -blinker = "*" -Flask = "*" - -[[package]] -name = "flask-marshmallow" -version = "0.15.0" -description = "Flask + marshmallow for beautiful APIs" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +flask-marshmallow = [ {file = "flask-marshmallow-0.15.0.tar.gz", hash = "sha256:2083ae55bebb5142fff98c6bbd483a2f5dbc531a8bc1be2180ed5f75e7f3fccc"}, {file = "flask_marshmallow-0.15.0-py2.py3-none-any.whl", hash = "sha256:ce08a153f74da6ebfffd8065d1687508b0179df37fff7fc0c8f28ccfb64f1b56"}, ] - -[package.dependencies] -Flask = "*" -marshmallow = ">=3.0.0" -packaging = ">=17.0" - -[package.extras] -dev = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.28.2)", "mock", "pre-commit", "pytest", "tox"] -docs = ["Sphinx (==6.1.3)", "marshmallow-sqlalchemy (>=0.13.0)", "sphinx-issues (==3.0.1)"] -sqlalchemy = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.28.2)"] -tests = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.28.2)", "mock", "pytest"] - -[[package]] -name = "flask-migrate" -version = "4.0.4" -description = "SQLAlchemy database migrations for Flask applications using Alembic." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +flask-migrate = [ {file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"}, {file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"}, ] - -[package.dependencies] -alembic = ">=1.9.0" -Flask = ">=0.9" -Flask-SQLAlchemy = ">=1.0" - -[[package]] -name = "flask-restful" -version = "0.3.9" -description = "Simple framework for creating REST APIs" -category = "main" -optional = false -python-versions = "*" -files = [ +flask-restful = [ {file = "Flask-RESTful-0.3.9.tar.gz", hash = "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e"}, {file = "Flask_RESTful-0.3.9-py2.py3-none-any.whl", hash = "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2"}, ] - -[package.dependencies] -aniso8601 = ">=0.82" -Flask = ">=0.8" -pytz = "*" -six = ">=1.3.0" - -[package.extras] -docs = ["sphinx"] - -[[package]] -name = "flask-simple-crypt" -version = "0.3.3" -description = "Flask extension based on simple-crypt that allows simple, secure encryption and decryption for Python." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +flask-simple-crypt = [ {file = "Flask-Simple-Crypt-0.3.3.tar.gz", hash = "sha256:0d4033b6c9a03ac85d10f0fd213914390217dc53b2d41d153fa050fee9723594"}, {file = "Flask_Simple_Crypt-0.3.3-py3-none-any.whl", hash = "sha256:08c3fcad955ac148bb885b1de4798c1cfce8512452072beee414bacf1552e8ef"}, ] - -[package.dependencies] -Flask = "*" -pycryptodome = "*" - -[[package]] -name = "flask-sqlalchemy" -version = "3.0.3" -description = "Add SQLAlchemy support to your Flask application." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +flask-sqlalchemy = [ {file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"}, {file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"}, ] - -[package.dependencies] -Flask = ">=2.2" -SQLAlchemy = ">=1.4.18" - -[[package]] -name = "furo" -version = "2023.3.27" -description = "A clean customisable Sphinx documentation theme." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "furo-2023.3.27-py3-none-any.whl", hash = "sha256:4ab2be254a2d5e52792d0ca793a12c35582dd09897228a6dd47885dabd5c9521"}, - {file = "furo-2023.3.27.tar.gz", hash = "sha256:b99e7867a5cc833b2b34d7230631dd6558c7a29f93071fdbb5709634bb33c5a5"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7" -sphinx = ">=5.0,<7.0" -sphinx-basic-ng = "*" - -[[package]] -name = "gitdb" -version = "4.0.10" -description = "Git Object Database" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +gitdb = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, ] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.31" -description = "GitPython is a Python library used to interact with Git repositories" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +gitpython = [ {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"}, {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"}, ] - -[package.dependencies] -gitdb = ">=4.0.1,<5" - -[[package]] -name = "greenlet" -version = "2.0.2" -description = "Lightweight in-process concurrent programming" -category = "main" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -files = [ +greenlet = [ {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, @@ -1202,191 +2349,47 @@ files = [ {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, ] - -[package.extras] -docs = ["Sphinx", "docutils (<0.18)"] -test = ["objgraph", "psutil"] - -[[package]] -name = "gunicorn" -version = "20.1.0" -description = "WSGI HTTP Server for UNIX" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] - -[package.dependencies] -setuptools = ">=3.0" - -[package.extras] -eventlet = ["eventlet (>=0.24.1)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -tornado = ["tornado (>=0.2)"] - -[[package]] -name = "identify" -version = "2.5.22" -description = "File identification library for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +identify = [ {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, ] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.2.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +importlib-metadata = [ {file = "importlib_metadata-6.2.0-py3-none-any.whl", hash = "sha256:8388b74023a138c605fddd0d47cb81dd706232569f56c9aca7d9c7fdb54caeba"}, {file = "importlib_metadata-6.2.0.tar.gz", hash = "sha256:9127aad2f49d7203e7112098c12b92e4fd1061ccd18548cdfdc49171a8c073cc"}, ] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "inflection" -version = "0.5.1" -description = "A port of Ruby on Rails inflector to Python" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +inflection = [ {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, ] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +iniconfig = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] - -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -category = "main" -optional = false -python-versions = ">=3.8.0" -files = [ +isort = [ {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, ] - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - -[[package]] -name = "itsdangerous" -version = "2.1.2" -description = "Safely pass data to untrusted environments and back." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +itsdangerous = [ {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, ] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jsonschema" -version = "4.17.3" -description = "An implementation of JSON Schema validation for Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +jsonschema = [ {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, ] - -[package.dependencies] -attrs = ">=17.4.0" -pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "lazy-object-proxy" -version = "1.9.0" -description = "A fast and thorough lazy object proxy." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +lazy-object-proxy = [ {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, @@ -1424,31 +2427,7 @@ files = [ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, ] - -[[package]] -name = "livereload" -version = "2.6.3" -description = "Python LiveReload is an awesome tool for web developers" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, - {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, -] - -[package.dependencies] -six = "*" -tornado = {version = "*", markers = "python_version > \"2.7\""} - -[[package]] -name = "lxml" -version = "4.9.2" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" -files = [ +lxml = [ {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, @@ -1527,41 +2506,11 @@ files = [ {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, ] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.7)"] - -[[package]] -name = "mako" -version = "1.2.4" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +mako = [ {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, ] - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["Babel"] -lingua = ["lingua"] -testing = ["pytest"] - -[[package]] -name = "markupsafe" -version = "2.1.2" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +markupsafe = [ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, @@ -1613,86 +2562,23 @@ files = [ {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] - -[[package]] -name = "marshmallow" -version = "3.19.0" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +marshmallow = [ {file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"}, {file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"}, ] - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"] -tests = ["pytest", "pytz", "simplejson"] - -[[package]] -name = "marshmallow-enum" -version = "1.5.1" -description = "Enum field for Marshmallow" -category = "main" -optional = false -python-versions = "*" -files = [ +marshmallow-enum = [ {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"}, ] - -[package.dependencies] -marshmallow = ">=2.0.0" - -[[package]] -name = "marshmallow-sqlalchemy" -version = "0.29.0" -description = "SQLAlchemy integration with the marshmallow (de)serialization library" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +marshmallow-sqlalchemy = [ {file = "marshmallow-sqlalchemy-0.29.0.tar.gz", hash = "sha256:3523a774390ef0c1c0f7c708a7519809c5396cf608720f14f55c36f74ff5bbec"}, {file = "marshmallow_sqlalchemy-0.29.0-py2.py3-none-any.whl", hash = "sha256:3cee0bf61ed10687c0a41448e1916649b28222334a02f7b937c39d1c69c18bee"}, ] - -[package.dependencies] -marshmallow = ">=3.0.0" -packaging = ">=21.3" -SQLAlchemy = ">=1.4.40,<3.0" - -[package.extras] -dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.2.13)", "pre-commit (==3.1.0)", "pytest", "pytest-lazy-fixture (>=0.6.2)", "tox"] -docs = ["alabaster (==0.7.13)", "sphinx (==6.1.3)", "sphinx-issues (==3.0.1)"] -lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.2.13)", "pre-commit (==3.1.0)"] -tests = ["pytest", "pytest-lazy-fixture (>=0.6.2)"] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "main" -optional = false -python-versions = "*" -files = [ +mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] - -[[package]] -name = "mypy" -version = "1.2.0" -description = "Optional static typing for Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +mypy = [ {file = "mypy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d"}, {file = "mypy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"}, {file = "mypy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e"}, @@ -1720,38 +2606,11 @@ files = [ {file = "mypy-1.2.0-py3-none-any.whl", hash = "sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394"}, {file = "mypy-1.2.0.tar.gz", hash = "sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1"}, ] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +mypy-extensions = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] - -[[package]] -name = "mysql-connector-python" -version = "8.0.32" -description = "MySQL driver written in Python" -category = "main" -optional = false -python-versions = "*" -files = [ +mysql-connector-python = [ {file = "mysql-connector-python-8.0.32.tar.gz", hash = "sha256:c2d20b29fd096a0633f9360c275bd2434d4bcf597281991c4b7f1c820cd07b84"}, {file = "mysql_connector_python-8.0.32-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4df11c683924ef34c177a54887dc4844ae735b01c8a29ce6ab92d6d3db7a2757"}, {file = "mysql_connector_python-8.0.32-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4b2d00c9e2cb9e3d11c57ec411226f43aa627607085fbed661cfea1c4dc57f61"}, @@ -1778,190 +2637,51 @@ files = [ {file = "mysql_connector_python-8.0.32-cp39-cp39-win_amd64.whl", hash = "sha256:8c334c41cd1c5bcfa3550340253ef7d9d3b962211f33327c20f69706a0bcce06"}, {file = "mysql_connector_python-8.0.32-py2.py3-none-any.whl", hash = "sha256:e0299236297b63bf6cbb61d81a9d400bc01cad4743d1abe5296ef349de15ee53"}, ] - -[package.dependencies] -protobuf = ">=3.11.0,<=3.20.3" - -[package.extras] -compression = ["lz4 (>=2.1.6,<=3.1.3)", "zstandard (>=0.12.0,<=0.19.0)"] -dns-srv = ["dnspython (>=1.16.0,<=2.1.0)"] -gssapi = ["gssapi (>=1.6.9,<=1.8.2)"] - -[[package]] -name = "nodeenv" -version = "1.7.0" -description = "Node.js virtual environment builder" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ +nodeenv = [ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pathspec = [ {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, ] - -[[package]] -name = "pbr" -version = "5.11.1" -description = "Python Build Reasonableness" -category = "dev" -optional = false -python-versions = ">=2.6" -files = [ +pbr = [ {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, ] - -[[package]] -name = "pep8-naming" -version = "0.13.2" -description = "Check PEP-8 naming conventions, plugin for flake8" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pep8-naming = [ {file = "pep8-naming-0.13.2.tar.gz", hash = "sha256:93eef62f525fd12a6f8c98f4dcc17fa70baae2f37fa1f73bec00e3e44392fa48"}, {file = "pep8_naming-0.13.2-py3-none-any.whl", hash = "sha256:59e29e55c478db69cffbe14ab24b5bd2cd615c0413edf790d47d3fb7ba9a4e23"}, ] - -[package.dependencies] -flake8 = ">=3.9.1" - -[[package]] -name = "platformdirs" -version = "3.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +platformdirs = [ {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, ] - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "2.21.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pre-commit = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pre-commit-hooks" -version = "4.4.0" -description = "Some out-of-the-box hooks for pre-commit." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pre-commit-hooks = [ {file = "pre_commit_hooks-4.4.0-py2.py3-none-any.whl", hash = "sha256:fc8837335476221ccccda3d176ed6ae29fe58753ce7e8b7863f5d0f987328fc6"}, {file = "pre_commit_hooks-4.4.0.tar.gz", hash = "sha256:7011eed8e1a25cde94693da009cba76392194cecc2f3f06c51a44ea6ad6c2af9"}, ] - -[package.dependencies] -"ruamel.yaml" = ">=0.15" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "prometheus-client" -version = "0.16.0" -description = "Python client for the Prometheus monitoring system." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +prometheus-client = [ {file = "prometheus_client-0.16.0-py3-none-any.whl", hash = "sha256:0836af6eb2c8f4fed712b2f279f6c0a8bbab29f9f4aa15276b91c7cb0d1616ab"}, {file = "prometheus_client-0.16.0.tar.gz", hash = "sha256:a03e35b359f14dd1630898543e2120addfdeacd1a6069c1367ae90fd93ad3f48"}, ] - -[package.extras] -twisted = ["twisted"] - -[[package]] -name = "prometheus-flask-exporter" -version = "0.22.3" -description = "Prometheus metrics exporter for Flask" -category = "main" -optional = false -python-versions = "*" -files = [ +prometheus-flask-exporter = [ {file = "prometheus_flask_exporter-0.22.3-py3-none-any.whl", hash = "sha256:16e6a3a7ce0089fc7c78a6956cdf28c184c3ac518e2b46a2a8e410b68d3a84a3"}, {file = "prometheus_flask_exporter-0.22.3.tar.gz", hash = "sha256:32b152aeb7970cbf04616627fc5bf20d82b0918e54c54f80dc8aaef3349fd333"}, ] - -[package.dependencies] -flask = "*" -prometheus-client = "*" - -[[package]] -name = "protobuf" -version = "3.20.3" -description = "Protocol Buffers" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +protobuf = [ {file = "protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99"}, {file = "protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e"}, {file = "protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c"}, @@ -1985,15 +2705,7 @@ files = [ {file = "protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db"}, {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, ] - -[[package]] -name = "psycopg2" -version = "2.9.6" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +psycopg2 = [ {file = "psycopg2-2.9.6-cp310-cp310-win32.whl", hash = "sha256:f7a7a5ee78ba7dc74265ba69e010ae89dae635eea0e97b055fb641a01a31d2b1"}, {file = "psycopg2-2.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:f75001a1cbbe523e00b0ef896a5a1ada2da93ccd752b7636db5a99bc57c44494"}, {file = "psycopg2-2.9.6-cp311-cp311-win32.whl", hash = "sha256:53f4ad0a3988f983e9b49a5d9765d663bbe84f508ed655affdb810af9d0972ad"}, @@ -2008,39 +2720,15 @@ files = [ {file = "psycopg2-2.9.6-cp39-cp39-win_amd64.whl", hash = "sha256:ded2faa2e6dfb430af7713d87ab4abbfc764d8d7fb73eafe96a24155f906ebf5"}, {file = "psycopg2-2.9.6.tar.gz", hash = "sha256:f15158418fd826831b28585e2ab48ed8df2d0d98f502a2b4fe619e7d5ca29011"}, ] - -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ +pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ +pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] - -[[package]] -name = "pycryptodome" -version = "3.17" -description = "Cryptographic library for Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ +pycryptodome = [ {file = "pycryptodome-3.17-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:2c5631204ebcc7ae33d11c43037b2dafe25e2ab9c1de6448eb6502ac69c19a56"}, {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:04779cc588ad8f13c80a060b0b1c9d1c203d051d8a43879117fe6b8aaf1cd3fa"}, {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f812d58c5af06d939b2baccdda614a3ffd80531a26e5faca2c9f8b1770b2b7af"}, @@ -2075,123 +2763,31 @@ files = [ {file = "pycryptodome-3.17-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:74794a2e2896cd0cf56fdc9db61ef755fa812b4a4900fa46c49045663a92b8d0"}, {file = "pycryptodome-3.17.tar.gz", hash = "sha256:bce2e2d8e82fcf972005652371a3e8731956a0c1fbb719cc897943b3695ad91b"}, ] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +pydocstyle = [ {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, ] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3)"] - -[[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ +pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] - -[[package]] -name = "pygments" -version = "2.14.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +pygments = [ {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, ] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pyjwt" -version = "2.6.0" -description = "JSON Web Token implementation in Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +pyjwt = [ {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, ] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pylint" -version = "2.17.2" -description = "python code static checker" -category = "main" -optional = false -python-versions = ">=3.7.2" -files = [ +pylint = [ {file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"}, {file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"}, ] - -[package.dependencies] -astroid = ">=2.15.2,<=2.17.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, -] -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" -files = [ +pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pyrsistent" -version = "0.19.3" -description = "Persistent/Functional/Immutable data structures" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +pyrsistent = [ {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, @@ -2220,156 +2816,39 @@ files = [ {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, ] - -[[package]] -name = "pytest" -version = "7.2.2" -description = "pytest: simple powerful testing with Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +pytest = [ {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, ] - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pytest-flask" -version = "1.2.0" -description = "A set of py.test fixtures to test Flask applications." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +pytest-flask = [ {file = "pytest-flask-1.2.0.tar.gz", hash = "sha256:46fde652f77777bf02dc91205aec4ce20cdf2acbbbd66a918ab91f5c14693d3d"}, {file = "pytest_flask-1.2.0-py3-none-any.whl", hash = "sha256:fe25b39ad0db09c3d1fe728edecf97ced85e774c775db259a6d25f0270a4e7c9"}, ] - -[package.dependencies] -Flask = "*" -pytest = ">=5.2" -Werkzeug = ">=0.7" - -[package.extras] -docs = ["Sphinx", "sphinx-rtd-theme"] - -[[package]] -name = "pytest-flask-sqlalchemy" -version = "1.1.0" -description = "A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions." -category = "main" -optional = false -python-versions = "*" -files = [ +pytest-flask-sqlalchemy = [ {file = "pytest-flask-sqlalchemy-1.1.0.tar.gz", hash = "sha256:db71a57b90435e5d854b21c37a2584056d6fc3ddb28c09d8d0a2546bd6e390ff"}, {file = "pytest_flask_sqlalchemy-1.1.0-py3-none-any.whl", hash = "sha256:b9f272d5c4092fcbe4a6284e402a37cad84f5b9be3c0bbe1a11927f24c99ff83"}, ] - -[package.dependencies] -Flask-SQLAlchemy = ">=2.3" -packaging = ">=14.1" -pytest = ">=3.2.1" -pytest-mock = ">=1.6.2" -SQLAlchemy = ">=1.2.2" - -[package.extras] -tests = ["psycopg2-binary", "pytest (>=6.0.1)", "pytest-postgresql (>=2.4.0,<4.0.0)"] - -[[package]] -name = "pytest-mock" -version = "3.10.0" -description = "Thin-wrapper around the mock package for easier use with pytest" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +pytest-mock = [ {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, ] - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ +python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" -files = [ +pytz = [ {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, ] - -[[package]] -name = "pytz-deprecation-shim" -version = "0.1.0.post0" -description = "Shims to make deprecation of pytz easier" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ +pytz-deprecation-shim = [ {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, ] - -[package.dependencies] -tzdata = {version = "*", markers = "python_version >= \"3.6\""} - -[[package]] -name = "pyupgrade" -version = "3.3.1" -description = "A tool to automatically upgrade syntax for newer versions." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pyupgrade = [ {file = "pyupgrade-3.3.1-py2.py3-none-any.whl", hash = "sha256:3b93641963df022d605c78aeae4b5956a5296ea24701eafaef9c487527b77e60"}, {file = "pyupgrade-3.3.1.tar.gz", hash = "sha256:f88bce38b0ba92c2a9a5063c8629e456e8d919b67d2d42c7ecab82ff196f9813"}, ] - -[package.dependencies] -tokenize-rt = ">=3.2.0" - -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -2411,15 +2890,7 @@ files = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] - -[[package]] -name = "regex" -version = "2023.3.23" -description = "Alternative regular expression module, to replace re." -category = "main" -optional = false -python-versions = ">=3.8" -files = [ +regex = [ {file = "regex-2023.3.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:845a5e2d84389c4ddada1a9b95c055320070f18bb76512608374aca00d22eca8"}, {file = "regex-2023.3.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87d9951f5a538dd1d016bdc0dcae59241d15fa94860964833a54d18197fcd134"}, {file = "regex-2023.3.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae17d3be44c0b3f782c28ae9edd8b47c1f1776d4cabe87edc0b98e1f12b021"}, @@ -2481,101 +2952,26 @@ files = [ {file = "regex-2023.3.23-cp39-cp39-win_amd64.whl", hash = "sha256:54c3fa855a3f7438149de3211738dd9b5f0c733f48b54ae05aa7fce83d48d858"}, {file = "regex-2023.3.23.tar.gz", hash = "sha256:dc80df325b43ffea5cdea2e3eaa97a44f3dd298262b1c7fe9dbb2a9522b956a7"}, ] - -[[package]] -name = "reorder-python-imports" -version = "3.9.0" -description = "Tool for reordering python imports" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +reorder-python-imports = [ {file = "reorder_python_imports-3.9.0-py2.py3-none-any.whl", hash = "sha256:3f9c16e8781f54c944756d0d1eb34a8c863554f7a4eb3693f574fe19b1a29b56"}, {file = "reorder_python_imports-3.9.0.tar.gz", hash = "sha256:49292ed537829a6bece9fb3746fc1bbe98f52643be5de01a4e13680268a5b0ec"}, ] - -[package.dependencies] -classify-imports = ">=4.1" - -[[package]] -name = "requests" -version = "2.28.2" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" -files = [ +requests = [ {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, ] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "restrictedpython" -version = "6.0" -description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment." -category = "main" -optional = false -python-versions = ">=3.6, <3.12" -files = [ +restrictedpython = [ {file = "RestrictedPython-6.0-py3-none-any.whl", hash = "sha256:3479303f7bff48a7dedad76f96e7704993c5e86c5adbd67f607295d5352f0fb8"}, {file = "RestrictedPython-6.0.tar.gz", hash = "sha256:405cf0bd9eec2f19b1326b5f48228efe56d6590b4e91826b8cc3b2cd400a96ad"}, ] - -[package.extras] -docs = ["Sphinx", "sphinx-rtd-theme"] -test = ["pytest", "pytest-mock"] - -[[package]] -name = "restructuredtext-lint" -version = "1.4.0" -description = "reStructuredText linter" -category = "dev" -optional = false -python-versions = "*" -files = [ +restructuredtext-lint = [ {file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"}, ] - -[package.dependencies] -docutils = ">=0.11,<1.0" - -[[package]] -name = "ruamel-yaml" -version = "0.17.21" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -category = "main" -optional = false -python-versions = ">=3" -files = [ +ruamel-yaml = [ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, ] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} - -[package.extras] -docs = ["ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.7" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ +ruamel-yaml-clib = [ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, @@ -2585,8 +2981,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, @@ -2613,98 +3007,19 @@ files = [ {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, ] - -[[package]] -name = "safety" -version = "2.3.5" -description = "Checks installed dependencies for known vulnerabilities and licenses." -category = "main" -optional = false -python-versions = "*" -files = [ +safety = [ {file = "safety-2.3.5-py3-none-any.whl", hash = "sha256:2227fcac1b22b53c1615af78872b48348661691450aa25d6704a5504dbd1f7e2"}, {file = "safety-2.3.5.tar.gz", hash = "sha256:a60c11f8952f412cbb165d70cb1f673a3b43a2ba9a93ce11f97e6a4de834aa3a"}, ] - -[package.dependencies] -Click = ">=8.0.2" -dparse = ">=0.6.2" -packaging = ">=21.0,<22.0" -requests = "*" -"ruamel.yaml" = ">=0.17.21" -setuptools = ">=19.3" - -[package.extras] -github = ["jinja2 (>=3.1.0)", "pygithub (>=1.43.3)"] -gitlab = ["python-gitlab (>=1.3.0)"] - -[[package]] -name = "sentry-sdk" -version = "1.19.1" -description = "Python client for Sentry (https://sentry.io)" -category = "main" -optional = false -python-versions = "*" -files = [ +sentry-sdk = [ {file = "sentry-sdk-1.19.1.tar.gz", hash = "sha256:7ae78bd921981a5010ab540d6bdf3b793659a4db8cccf7f16180702d48a80d84"}, {file = "sentry_sdk-1.19.1-py2.py3-none-any.whl", hash = "sha256:885a11c69df23e53eb281d003b9ff15a5bdfa43d8a2a53589be52104a1b4582f"}, ] - -[package.dependencies] -certifi = "*" -urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} - -[package.extras] -aiohttp = ["aiohttp (>=3.5)"] -arq = ["arq (>=0.23)"] -beam = ["apache-beam (>=2.12)"] -bottle = ["bottle (>=0.12.13)"] -celery = ["celery (>=3)"] -chalice = ["chalice (>=1.16.0)"] -django = ["django (>=1.8)"] -falcon = ["falcon (>=1.4)"] -fastapi = ["fastapi (>=0.79.0)"] -flask = ["blinker (>=1.1)", "flask (>=0.11)"] -grpcio = ["grpcio (>=1.21.1)"] -httpx = ["httpx (>=0.16.0)"] -huey = ["huey (>=2)"] -opentelemetry = ["opentelemetry-distro (>=0.35b0)"] -pure-eval = ["asttokens", "executing", "pure-eval"] -pymongo = ["pymongo (>=3.1)"] -pyspark = ["pyspark (>=2.4.4)"] -quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] -rq = ["rq (>=0.6)"] -sanic = ["sanic (>=0.8)"] -sqlalchemy = ["sqlalchemy (>=1.2)"] -starlette = ["starlette (>=0.19.1)"] -starlite = ["starlite (>=1.48)"] -tornado = ["tornado (>=5)"] - -[[package]] -name = "setuptools" -version = "65.7.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +setuptools = [ {file = "setuptools-65.7.0-py3-none-any.whl", hash = "sha256:8ab4f1dbf2b4a65f7eec5ad0c620e84c34111a68d3349833494b9088212214dd"}, {file = "setuptools-65.7.0.tar.gz", hash = "sha256:4d3c92fac8f1118bb77a22181355e29c239cabfe2b9effdaa665c66b711136d7"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "simplejson" -version = "3.19.1" -description = "Simple, fast, extensible JSON encoder/decoder for Python" -category = "main" -optional = false -python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +simplejson = [ {file = "simplejson-3.19.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:412e58997a30c5deb8cab5858b8e2e5b40ca007079f7010ee74565cc13d19665"}, {file = "simplejson-3.19.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e765b1f47293dedf77946f0427e03ee45def2862edacd8868c6cf9ab97c8afbd"}, {file = "simplejson-3.19.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3231100edee292da78948fa0a77dee4e5a94a0a60bcba9ed7a9dc77f4d4bb11e"}, @@ -2791,273 +3106,19 @@ files = [ {file = "simplejson-3.19.1-py3-none-any.whl", hash = "sha256:4710806eb75e87919b858af0cba4ffedc01b463edc3982ded7b55143f39e41e1"}, {file = "simplejson-3.19.1.tar.gz", hash = "sha256:6277f60848a7d8319d27d2be767a7546bc965535b28070e310b3a9af90604a4c"}, ] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] - -[[package]] -name = "smmap" -version = "5.0.0" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +smmap = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" -optional = false -python-versions = "*" -files = [ +snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] - -[[package]] -name = "soupsieve" -version = "2.4" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"}, - {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"}, -] - -[[package]] -name = "sphinx" -version = "5.3.0" -description = "Python documentation generator" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, -] - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" -imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] - -[[package]] -name = "sphinx-autoapi" -version = "2.1.0" -description = "Sphinx API documentation generator" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx-autoapi-2.1.0.tar.gz", hash = "sha256:5b5c58064214d5a846c9c81d23f00990a64654b9bca10213231db54a241bc50f"}, - {file = "sphinx_autoapi-2.1.0-py2.py3-none-any.whl", hash = "sha256:b25c7b2cda379447b8c36b6a0e3bdf76e02fd64f7ca99d41c6cbdf130a01768f"}, -] - -[package.dependencies] -astroid = ">=2.7" -Jinja2 = "*" -PyYAML = "*" -sphinx = ">=5.2.0" -unidecode = "*" - -[package.extras] -docs = ["sphinx", "sphinx-rtd-theme"] -dotnet = ["sphinxcontrib-dotnetdomain"] -go = ["sphinxcontrib-golangdomain"] - -[[package]] -name = "sphinx-autobuild" -version = "2021.3.14" -description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, - {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, -] - -[package.dependencies] -colorama = "*" -livereload = "*" -sphinx = "*" - -[package.extras] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b1" -description = "A modern skeleton for Sphinx themes." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx_basic_ng-1.0.0b1-py3-none-any.whl", hash = "sha256:ade597a3029c7865b24ad0eda88318766bcc2f9f4cef60df7e28126fde94db2a"}, - {file = "sphinx_basic_ng-1.0.0b1.tar.gz", hash = "sha256:89374bd3ccd9452a301786781e28c8718e99960f2d4f411845ea75fc7bb5a9b0"}, -] - -[package.dependencies] -sphinx = ">=4.0" - -[package.extras] -docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] - -[[package]] -name = "sphinx-click" -version = "4.4.0" -description = "Sphinx extension that automatically documents click applications" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx-click-4.4.0.tar.gz", hash = "sha256:cc67692bd28f482c7f01531c61b64e9d2f069bfcf3d24cbbb51d4a84a749fa48"}, - {file = "sphinx_click-4.4.0-py3-none-any.whl", hash = "sha256:2821c10a68fc9ee6ce7c92fad26540d8d8c8f45e6d7258f0e4fb7529ae8fab49"}, -] - -[package.dependencies] -click = ">=7.0" -docutils = "*" -sphinx = ">=2.0" - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.4" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.1" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "spiff-element-units" -version = "0.3.0" -description = "" -category = "main" -optional = false -python-versions = ">=3.9" -files = [ +spiff-element-units = [ {file = "spiff_element_units-0.3.0-cp39-abi3-macosx_10_7_x86_64.whl", hash = "sha256:35d2ede30e4d2fc715882c44b19e70e7289e1bb6fdff213198ba1e5d073987d1"}, {file = "spiff_element_units-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:d2a9dde576144ba040ca7fb11ee5487e7c7507b8ee7f1c6c12d24066b67c24c7"}, {file = "spiff_element_units-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e6d02e5fd59c99484afec41f7f96272e38b7f933e17be9a29b2e3bd43b8396"}, @@ -3070,35 +3131,8 @@ files = [ {file = "spiff_element_units-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:beacf2857ba196b39a66f481e9989513e4df607e6d2157d4c1c1e463890ca806"}, {file = "spiff_element_units-0.3.0.tar.gz", hash = "sha256:5c740c70adf7e0fc39c9a3a199c4f1d5d5941d61b28c90637b64df4dc3df57dd"}, ] - -[[package]] -name = "SpiffWorkflow" -version = "1.2.1" -description = "A workflow framework and BPMN/DMN Processor" -category = "main" -optional = false -python-versions = "*" -files = [] -develop = false - -[package.dependencies] -configparser = "*" -lxml = "*" - -[package.source] -type = "git" -url = "https://github.com/sartography/SpiffWorkflow" -reference = "main" -resolved_reference = "340e9983b5afd2e6e71df883e74f7dc20d4474dd" - -[[package]] -name = "sqlalchemy" -version = "2.0.9" -description = "Database Abstraction Library" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +SpiffWorkflow = [] +sqlalchemy = [ {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"}, @@ -3141,423 +3175,100 @@ files = [ {file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"}, {file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"}, ] - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} -typing-extensions = ">=4.2.0" - -[package.extras] -aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=7)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] - -[[package]] -name = "sqlalchemy-stubs" -version = "0.4" -description = "SQLAlchemy stubs and mypy plugin" -category = "main" -optional = false -python-versions = "*" -files = [] -develop = false - -[package.dependencies] -mypy = ">=0.790" -typing-extensions = ">=3.7.4" - -[package.source] -type = "git" -url = "https://github.com/burnettk/sqlalchemy-stubs.git" -reference = "scoped-session-delete" -resolved_reference = "d1176931684ce5b327539cc9567d4a1cd8ef1efd" - -[[package]] -name = "stevedore" -version = "5.0.0" -description = "Manage dynamic plugins for Python applications" -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ +sqlalchemy-stubs = [] +stevedore = [ {file = "stevedore-5.0.0-py3-none-any.whl", hash = "sha256:bd5a71ff5e5e5f5ea983880e4a1dd1bb47f8feebbb3d95b592398e2f02194771"}, {file = "stevedore-5.0.0.tar.gz", hash = "sha256:2c428d2338976279e8eb2196f7a94910960d9f7ba2f41f3988511e95ca447021"}, ] - -[package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" - -[[package]] -name = "swagger-ui-bundle" -version = "0.0.9" -description = "swagger_ui_bundle - swagger-ui files in a pip package" -category = "main" -optional = false -python-versions = "*" -files = [ +swagger-ui-bundle = [ {file = "swagger_ui_bundle-0.0.9-py3-none-any.whl", hash = "sha256:cea116ed81147c345001027325c1ddc9ca78c1ee7319935c3c75d3669279d575"}, {file = "swagger_ui_bundle-0.0.9.tar.gz", hash = "sha256:b462aa1460261796ab78fd4663961a7f6f347ce01760f1303bbbdf630f11f516"}, ] - -[package.dependencies] -Jinja2 = ">=2.0" - -[[package]] -name = "tokenize-rt" -version = "5.0.0" -description = "A wrapper around the stdlib `tokenize` which roundtrips." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +tokenize-rt = [ {file = "tokenize_rt-5.0.0-py2.py3-none-any.whl", hash = "sha256:c67772c662c6b3dc65edf66808577968fb10badfc2042e3027196bed4daf9e5a"}, {file = "tokenize_rt-5.0.0.tar.gz", hash = "sha256:3160bc0c3e8491312d0485171dea861fc160a240f5f5766b72a1165408d10740"}, ] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] - -[[package]] -name = "tomlkit" -version = "0.11.7" -description = "Style preserving TOML library" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +tomlkit = [ {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, ] - -[[package]] -name = "tornado" -version = "6.2" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -category = "dev" -optional = false -python-versions = ">= 3.7" -files = [ - {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, - {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, - {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, - {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, - {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, -] - -[[package]] -name = "typeguard" -version = "3.0.2" -description = "Run-time type checker for Python" -category = "dev" -optional = false -python-versions = ">=3.7.4" -files = [ +typeguard = [ {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 = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["mypy (>=0.991)", "pytest (>=7)"] - -[[package]] -name = "types-click" -version = "7.1.8" -description = "Typing stubs for click" -category = "main" -optional = false -python-versions = "*" -files = [ +types-click = [ {file = "types-click-7.1.8.tar.gz", hash = "sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092"}, {file = "types_click-7.1.8-py3-none-any.whl", hash = "sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81"}, ] - -[[package]] -name = "types-dateparser" -version = "1.1.4.9" -description = "Typing stubs for dateparser" -category = "main" -optional = false -python-versions = "*" -files = [ +types-dateparser = [ {file = "types-dateparser-1.1.4.9.tar.gz", hash = "sha256:506668f024c2136a44e9046ee18dd4279a55df1be5dc55e5c29ab07643a2e18a"}, {file = "types_dateparser-1.1.4.9-py3-none-any.whl", hash = "sha256:6539e49032151a8445092109f93e61f51b2082a9f295691df13e073c6abf9137"}, ] - -[[package]] -name = "types-flask" -version = "1.1.6" -description = "Typing stubs for Flask" -category = "main" -optional = false -python-versions = "*" -files = [ +types-flask = [ {file = "types-Flask-1.1.6.tar.gz", hash = "sha256:aac777b3abfff9436e6b01f6d08171cf23ea6e5be71cbf773aaabb1c5763e9cf"}, {file = "types_Flask-1.1.6-py3-none-any.whl", hash = "sha256:6ab8a9a5e258b76539d652f6341408867298550b19b81f0e41e916825fc39087"}, ] - -[package.dependencies] -types-click = "*" -types-Jinja2 = "*" -types-Werkzeug = "*" - -[[package]] -name = "types-jinja2" -version = "2.11.9" -description = "Typing stubs for Jinja2" -category = "main" -optional = false -python-versions = "*" -files = [ +types-jinja2 = [ {file = "types-Jinja2-2.11.9.tar.gz", hash = "sha256:dbdc74a40aba7aed520b7e4d89e8f0fe4286518494208b35123bcf084d4b8c81"}, {file = "types_Jinja2-2.11.9-py3-none-any.whl", hash = "sha256:60a1e21e8296979db32f9374d8a239af4cb541ff66447bb915d8ad398f9c63b2"}, ] - -[package.dependencies] -types-MarkupSafe = "*" - -[[package]] -name = "types-markupsafe" -version = "1.1.10" -description = "Typing stubs for MarkupSafe" -category = "main" -optional = false -python-versions = "*" -files = [ +types-markupsafe = [ {file = "types-MarkupSafe-1.1.10.tar.gz", hash = "sha256:85b3a872683d02aea3a5ac2a8ef590193c344092032f58457287fbf8e06711b1"}, {file = "types_MarkupSafe-1.1.10-py3-none-any.whl", hash = "sha256:ca2bee0f4faafc45250602567ef38d533e877d2ddca13003b319c551ff5b3cc5"}, ] - -[[package]] -name = "types-pytz" -version = "2022.7.1.2" -description = "Typing stubs for pytz" -category = "main" -optional = false -python-versions = "*" -files = [ +types-pytz = [ {file = "types-pytz-2022.7.1.2.tar.gz", hash = "sha256:487d3e8e9f4071eec8081746d53fa982bbc05812e719dcbf2ebf3d55a1a4cd28"}, {file = "types_pytz-2022.7.1.2-py3-none-any.whl", hash = "sha256:40ca448a928d566f7d44ddfde0066e384f7ffbd4da2778e42a4570eaca572446"}, ] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.9" -description = "Typing stubs for PyYAML" -category = "main" -optional = false -python-versions = "*" -files = [ +types-pyyaml = [ {file = "types-PyYAML-6.0.12.9.tar.gz", hash = "sha256:c51b1bd6d99ddf0aa2884a7a328810ebf70a4262c292195d3f4f9a0005f9eeb6"}, {file = "types_PyYAML-6.0.12.9-py3-none-any.whl", hash = "sha256:5aed5aa66bd2d2e158f75dda22b059570ede988559f030cf294871d3b647e3e8"}, ] - -[[package]] -name = "types-requests" -version = "2.28.11.17" -description = "Typing stubs for requests" -category = "main" -optional = false -python-versions = "*" -files = [ +types-requests = [ {file = "types-requests-2.28.11.17.tar.gz", hash = "sha256:0d580652ce903f643f8c3b494dd01d29367ea57cea0c7ad7f65cf3169092edb0"}, {file = "types_requests-2.28.11.17-py3-none-any.whl", hash = "sha256:cc1aba862575019306b2ed134eb1ea994cab1c887a22e18d3383e6dd42e9789b"}, ] - -[package.dependencies] -types-urllib3 = "<1.27" - -[[package]] -name = "types-urllib3" -version = "1.26.25.10" -description = "Typing stubs for urllib3" -category = "main" -optional = false -python-versions = "*" -files = [ +types-urllib3 = [ {file = "types-urllib3-1.26.25.10.tar.gz", hash = "sha256:c44881cde9fc8256d05ad6b21f50c4681eb20092552351570ab0a8a0653286d6"}, {file = "types_urllib3-1.26.25.10-py3-none-any.whl", hash = "sha256:12c744609d588340a07e45d333bf870069fc8793bcf96bae7a96d4712a42591d"}, ] - -[[package]] -name = "types-werkzeug" -version = "1.0.9" -description = "Typing stubs for Werkzeug" -category = "main" -optional = false -python-versions = "*" -files = [ +types-werkzeug = [ {file = "types-Werkzeug-1.0.9.tar.gz", hash = "sha256:5cc269604c400133d452a40cee6397655f878fc460e03fde291b9e3a5eaa518c"}, {file = "types_Werkzeug-1.0.9-py3-none-any.whl", hash = "sha256:194bd5715a13c598f05c63e8a739328657590943bce941e8a3619a6b5d4a54ec"}, ] - -[[package]] -name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +typing-extensions = [ {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] - -[[package]] -name = "tzdata" -version = "2023.3" -description = "Provider of IANA time zone data" -category = "main" -optional = false -python-versions = ">=2" -files = [ +tzdata = [ {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, ] - -[[package]] -name = "tzlocal" -version = "4.3" -description = "tzinfo object for the local timezone" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +tzlocal = [ {file = "tzlocal-4.3-py3-none-any.whl", hash = "sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"}, {file = "tzlocal-4.3.tar.gz", hash = "sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355"}, ] - -[package.dependencies] -pytz-deprecation-shim = "*" -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] - -[[package]] -name = "unidecode" -version = "1.3.6" -description = "ASCII transliterations of Unicode text" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, - {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, -] - -[[package]] -name = "urllib3" -version = "1.26.15" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ +urllib3 = [ {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, ] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "virtualenv" -version = "20.21.0" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +virtualenv = [ {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, ] - -[package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "werkzeug" -version = "2.2.3" -description = "The comprehensive WSGI web application library." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +werkzeug = [ {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"}, {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"}, ] - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog"] - -[[package]] -name = "wrapt" -version = "1.15.0" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ +wrapt = [ {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, @@ -3634,72 +3345,15 @@ files = [ {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] - -[[package]] -name = "wtforms" -version = "3.0.1" -description = "Form validation and rendering for Python web development." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +wtforms = [ {file = "WTForms-3.0.1-py3-none-any.whl", hash = "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b"}, {file = "WTForms-3.0.1.tar.gz", hash = "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc"}, ] - -[package.dependencies] -MarkupSafe = "*" - -[package.extras] -email = ["email-validator"] - -[[package]] -name = "xdoctest" -version = "1.1.1" -description = "A rewrite of the builtin doctest module" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +xdoctest = [ {file = "xdoctest-1.1.1-py3-none-any.whl", hash = "sha256:d59d4ed91cb92e4430ef0ad1b134a2bef02adff7d2fb9c9f057547bee44081a2"}, {file = "xdoctest-1.1.1.tar.gz", hash = "sha256:2eac8131bdcdf2781b4e5a62d6de87f044b730cc8db8af142a51bb29c245e779"}, ] - -[package.dependencies] -colorama = {version = "*", optional = true, markers = "platform_system == \"Windows\" and extra == \"colors\""} -Pygments = {version = "*", optional = true, markers = "python_version >= \"3.5.0\" and extra == \"colors\""} -six = "*" - -[package.extras] -all = ["IPython", "IPython", "Pygments", "Pygments", "attrs", "codecov", "colorama", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert", "pyflakes", "pytest", "pytest", "pytest", "pytest-cov", "six", "tomli", "typing"] -all-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "codecov (==2.0.15)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "pyflakes (==2.2.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "six (==1.11.0)", "tomli (==0.2.0)", "typing (==3.7.4)"] -colors = ["Pygments", "Pygments", "colorama"] -jupyter = ["IPython", "IPython", "attrs", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert"] -optional = ["IPython", "IPython", "Pygments", "Pygments", "attrs", "colorama", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert", "pyflakes", "tomli"] -optional-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "pyflakes (==2.2.0)", "tomli (==0.2.0)"] -runtime-strict = ["six (==1.11.0)"] -tests = ["codecov", "pytest", "pytest", "pytest", "pytest-cov", "typing"] -tests-binary = ["cmake", "cmake", "ninja", "ninja", "pybind11", "pybind11", "scikit-build", "scikit-build"] -tests-binary-strict = ["cmake (==3.21.2)", "cmake (==3.25.0)", "ninja (==1.10.2)", "ninja (==1.11.1)", "pybind11 (==2.10.3)", "pybind11 (==2.7.1)", "scikit-build (==0.11.1)", "scikit-build (==0.16.1)"] -tests-strict = ["codecov (==2.0.15)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "typing (==3.7.4)"] - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +zipp = [ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.9,<3.12" -content-hash = "71d35a0ac0d7c680abd5cdcd8d28d08e8f582bc7ae9fc0fce2e0001bd3730930" diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index 2eed0721..d3d152fe 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -31,7 +31,7 @@ SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "ma # SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "6cad2981712bb61eca23af1adfafce02d3277cb9"} # SpiffWorkflow = {develop = true, path = "../../spiffworkflow/" } sentry-sdk = "^1.10" -sphinx-autoapi = "^2.0" +# sphinx-autoapi = "^2.0" mysql-connector-python = "*" pytest-flask = "^1.2.0" pytest-flask-sqlalchemy = "^1.1.0" @@ -93,8 +93,8 @@ safety = "^2.3.1" mypy = ">=0.961" typeguard = "^3" xdoctest = {extras = ["colors"], version = "^1.0.1"} -sphinx = "^5.0.2" -sphinx-autobuild = ">=2021.3.14" +# sphinx = "^5.0.2" +# sphinx-autobuild = ">=2021.3.14" pre-commit = "^2.20.0" flake8 = "^4.0.1" black = ">=21.10b0" @@ -111,10 +111,10 @@ pep8-naming = "^0.13.2" darglint = "^1.8.1" reorder-python-imports = "^3.9.0" pre-commit-hooks = "^4.0.1" -sphinx-click = "^4.3.0" +# sphinx-click = "^4.3.0" Pygments = "^2.10.0" pyupgrade = "^3.1.0" -furo = ">=2021.11.12" +# furo = ">=2021.11.12" [tool.poetry.scripts] spiffworkflow-backend = "spiffworkflow_backend.__main__:main" From cf1e1a79d9dfb64ddaaede7903378bedde237dda Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 19 May 2023 07:05:58 -0400 Subject: [PATCH 14/60] avoid doing stuff outside of nox --- .github/workflows/backend_tests.yml | 31 ++++++++++++++++------------- spiffworkflow-backend/noxfile.py | 12 +++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.github/workflows/backend_tests.yml b/.github/workflows/backend_tests.yml index 9d36ebc3..80692cd1 100644 --- a/.github/workflows/backend_tests.yml +++ b/.github/workflows/backend_tests.yml @@ -119,20 +119,23 @@ jobs: pipx inject --pip-args=--constraint=.github/workflows/constraints.txt nox nox-poetry nox --version - - name: Checkout Samples - if: matrix.database == 'sqlite' - uses: actions/checkout@v3 - with: - repository: sartography/sample-process-models - path: sample-process-models - - name: Poetry Install - if: matrix.database == 'sqlite' - run: poetry install - - name: Setup sqlite - if: matrix.database == 'sqlite' - env: - SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR: "${GITHUB_WORKSPACE}/sample-process-models" - run: ./bin/recreate_db clean rmall + # when we get an imcompatible sqlite migration again and need to combine all migrations into one for the benefit of sqlite + # see if we can get the sqlite-specific block in the noxfile.py to work instead of this block in the github workflow, + # which annoyingly runs python setup outside of the nox environment (which seems to be flakier on poetry install). + # - name: Checkout Samples + # if: matrix.database == 'sqlite' + # uses: actions/checkout@v3 + # with: + # repository: sartography/sample-process-models + # path: sample-process-models + # - name: Poetry Install + # if: matrix.database == 'sqlite' + # run: poetry install + # - name: Setup sqlite + # if: matrix.database == 'sqlite' + # env: + # SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR: "${GITHUB_WORKSPACE}/sample-process-models" + # run: ./bin/recreate_db clean rmall - name: Setup Mysql uses: mirromutth/mysql-action@v1.1 diff --git a/spiffworkflow-backend/noxfile.py b/spiffworkflow-backend/noxfile.py index f266e411..2e67fbee 100644 --- a/spiffworkflow-backend/noxfile.py +++ b/spiffworkflow-backend/noxfile.py @@ -41,6 +41,18 @@ def setup_database(session: Session) -> None: session.env[flask_env_key] = "e7711a3ba96c46c68e084a86952de16f" session.env["FLASK_APP"] = "src/spiffworkflow_backend" session.env["SPIFFWORKFLOW_BACKEND_ENV"] = "unit_testing" + + if os.environ.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite": + # maybe replace this sqlite-specific block with ./bin/recreate_db clean rmall + # (if we can make it work, since it uses poetry), + # which would also remove the migrations folder and re-create things as a single migration + if os.path.exists("migrations"): + import shutil + + shutil.rmtree("migrations") + for task in ["init", "migrate"]: + session.run("flask", "db", task) + session.run("flask", "db", "upgrade") From 589d843da202116987dcbc5a66c763f24707a547 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 19 May 2023 09:15:19 -0400 Subject: [PATCH 15/60] remove docs-build matrix build --- .github/workflows/backend_tests.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backend_tests.yml b/.github/workflows/backend_tests.yml index 80692cd1..24ff8838 100644 --- a/.github/workflows/backend_tests.yml +++ b/.github/workflows/backend_tests.yml @@ -75,7 +75,7 @@ jobs: database: "sqlite", } - { python: "3.11", os: "ubuntu-latest", session: "xdoctest" } - - { python: "3.11", os: "ubuntu-latest", session: "docs-build" } + # - { python: "3.11", os: "ubuntu-latest", session: "docs-build" } env: FLASK_SESSION_SECRET_KEY: super_secret_key @@ -165,13 +165,13 @@ jobs: name: coverage-data path: "spiffworkflow-backend/.coverage.*" - - name: Upload documentation - if: matrix.session == 'docs-build' - uses: actions/upload-artifact@v3 - with: - name: docs - path: docs/_build - + # - name: Upload documentation + # if: matrix.session == 'docs-build' + # uses: actions/upload-artifact@v3 + # with: + # name: docs + # path: docs/_build + # - name: Upload logs if: failure() && matrix.session == 'tests' uses: "actions/upload-artifact@v3" From c5408bdacad3e1b21c5607b128f7c3d9496a0b19 Mon Sep 17 00:00:00 2001 From: Madhurya Liyanage Date: Fri, 19 May 2023 19:42:34 +0530 Subject: [PATCH 16/60] Commented out some unwanted lines --- .../cypress/pilot/NDR_PP1/softwarelicense.cy.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js index d68ff69a..625851e4 100644 --- a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js @@ -3371,12 +3371,13 @@ describe('Software and Licenses Path - With Files and Multiple items', () => { cy.get('body').click(); cy.get('#root_vendor').clear().type('Atlassian'); cy.get('#root_payment_method').select('Debit Card'); - cy.get('button') + /*cy.get('button') .contains(/^Submit$/) .click(); + + cy.contains('Task: Enter NDR Items', { timeout: 60000 });*/ // item 0 - cy.contains('Task: Enter NDR Items', { timeout: 60000 }); cy.get('#root_item_0_sub_category').select('op_src'); cy.get('#root_item_0_item_name') .clear() From d7a9eb237ee046df22ca6df2ce0ab16044f979d9 Mon Sep 17 00:00:00 2001 From: Madhurya Liyanage Date: Fri, 19 May 2023 19:44:19 +0530 Subject: [PATCH 17/60] Added some test cases for form validations --- .../pilot/NDR_PP1/initiaterequest.cy.js | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) diff --git a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/initiaterequest.cy.js b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/initiaterequest.cy.js index da77c1e1..7f88ad08 100644 --- a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/initiaterequest.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/initiaterequest.cy.js @@ -847,6 +847,251 @@ describe.only('Initiate a Request - Without Files', () => { }); }); +// Form validation - Software and License - Without Files +describe('Form validation', () => { + + //Special character check + it('Special character check', () => { + const username = Cypress.env('requestor_username'); + const password = Cypress.env('requestor_password'); + cy.log(`=====username : ${username}`); + cy.log(`=====password : ${password}`); + + cy.login(username, password); + cy.visit('/'); + + cy.contains('Start New +').click(); + cy.contains('Request Goods or Services').click(); + + cy.runPrimaryBpmnFile(true); + + /* cy.contains('Please select the type of request to start the process.'); + // wait a second to ensure we can click the radio button + + cy.wait(2000); + cy.get('input#root-procurement').click(); + cy.wait(2000); + + + cy.get('button') + .contains(/^Submit$/) + .click(); + */ + + cy.contains('Request Goods or Services', { timeout: 60000 }); + + cy.url().then((currentUrl) => { + // if url is "/tasks/8/d37c2f0f-016a-4066-b669-e0925b759560" + // extract the digits after /tasks + const processInstanceId = currentUrl.match(/(?<=\/tasks\/)\d+/)[0]; + cy.log('==###############===processInstanceId : ', processInstanceId); + const projectId = Cypress.env('project_id'); + cy.wait(2000); + cy.get('#root_project').select(projectId); + cy.get('#root_category').select('soft_and_lic'); + cy.get('#root_purpose') + .clear() + .type( + 'Purpose is to test all the special characters work in the request. ~!@#$%^&*()_+`-= {}[]\ and ;\',./:"<>? end. | this text goes missing', { parseSpecialCharSequences: false } + ); + //.type( + // 'Purpose is to test all the special characters work in the request. Test Special chars ~!@#$%^&*()_+`-= {}|[]\ and ;\',./:"<>? end.', { parseSpecialCharSequences: false } + //); + cy.get('#root_criticality').select('High'); + cy.get('#root_period').clear().type('25-11-2025'); + cy.get('body').click(); + cy.get('#root_vendor').clear().type('Microsoft'); + cy.get('#root_payment_method').select('Reimbursement'); + /* cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.contains('Task: Enter NDR Items', { timeout: 60000 }); +*/ + // item 0 + cy.get('#root_item_0_sub_category').select('op_src'); + cy.get('#root_item_0_item_name') + .clear() + .type( + 'Special char test ,./;\'[]\=-0987654321`~!@#$%^&*()_+{}:"<>? end.', { parseSpecialCharSequences: false } + ); + //.type( + // 'Special char test ,./;\'[]\=-0987654321`~!@#$%^&*()_+{}|:"<>? end.', { parseSpecialCharSequences: false } + // ); + cy.get('#root_item_0_qty').clear().type('2'); + cy.get('#root_item_0_currency_type').select('Crypto'); + cy.get('#root_item_0_currency').select('SNT'); + cy.get('#root_item_0_unit_price').type('1915'); + + cy.get('#root_item > div:nth-child(3) > p > button').click(); + + // item 1 + cy.get('#root_item_1_sub_category').select('lic_and_sub'); + cy.get('#root_item_1_item_name') + .clear() + .type( + 'Special char test 2 +_=)(*&^%$#@!~`?> {g} [a]end.', { parseSpecialCharSequences: false } + ); + //.type( + // 'Special char test 2 +_=)(*&^%$#@!~`?> {g} [a]end.', { parseSpecialCharSequences: false } + //); + cy.get('#root_item_1_qty').clear().type('1'); + cy.get('#root_item_1_currency_type').select('Fiat'); + cy.get('#root_item_1_currency').select('AED'); + cy.get('#root_item_1_unit_price').type('4500'); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.contains( + 'Review and provide any supporting information or files for your request.', + { timeout: 60000 } + ); + + cy.get('.cds--text-area__wrapper') + .find('#root') + .type( + 'Test Special chars afssfsfs,asfdf. sfsf? sfd/sfs f:sfsf " sfsdf; SDFfsd\' sfsdf{sfsfs} sfsdf[ sfsdf] fsfsfd\ sfsd sfsdf=S dfs+ sfd- sfsdf_ sfsfd (sfsd )sfsfsd * sf&sfsfs ^ sfs % sf $ ss# s@ sf! sfd` ss~ END.', { parseSpecialCharSequences: false } + ); + //.type( + // 'Test Special chars afssfsfs,asfdf. sfsf? sfd/sfs f:sfsf " sfsdf; SDFfsd\' sfsdf{sfsfs} sfsdf[ sfsdf] fsfsfd\ sfsd| sfsdf=S dfs+ sfd- sfsdf_ sfsfd (sfsd )sfsfsd * sf&sfsfs ^ sfs % sf $ ss# s@ sf! sfd` ss~ END.', { parseSpecialCharSequences: false } + //); + + // cy.contains('Submit the Request').click(); + + // cy.get('input[value="Submit the Request"]').click(); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.get('button') + .contains(/^Return to Home$/) + .click(); + + cy.contains('Started by me', { timeout: 60000 }); + cy.logout(); + cy.wait(2000); + }); + }); + + //Check field max lengths + it.only('Check field max lengths', () => { + const username = Cypress.env('requestor_username'); + const password = Cypress.env('requestor_password'); + cy.log(`=====username : ${username}`); + cy.log(`=====password : ${password}`); + + cy.login(username, password); + cy.visit('/'); + + cy.contains('Start New +').click(); + cy.contains('Request Goods or Services').click(); + + cy.runPrimaryBpmnFile(true); + + /* cy.contains('Please select the type of request to start the process.'); + // wait a second to ensure we can click the radio button + + cy.wait(2000); + cy.get('input#root-procurement').click(); + cy.wait(2000); + + + cy.get('button') + .contains(/^Submit$/) + .click(); + */ + + cy.contains('Request Goods or Services', { timeout: 60000 }); + + cy.url().then((currentUrl) => { + // if url is "/tasks/8/d37c2f0f-016a-4066-b669-e0925b759560" + // extract the digits after /tasks + const processInstanceId = currentUrl.match(/(?<=\/tasks\/)\d+/)[0]; + cy.log('==###############===processInstanceId : ', processInstanceId); + const projectId = Cypress.env('project_id'); + cy.wait(2000); + cy.get('#root_project').select(projectId); + cy.get('#root_category').select('soft_and_lic'); + cy.get('#root_purpose') + .clear() + .type( + 'Sware\nA software license is a document that provides legally binding guidelines for the use and distribution of software.\nSoftware licenses typically provide end users with the right to one or more copies of the software without violating copyrights. This is now more than 250 characters' + ); + cy.get('#root_criticality').select('High'); + cy.get('#root_period').clear().type('25-11-2025'); + cy.get('body').click(); + cy.get('#root_vendor').clear().type('Microsoft'); + cy.get('#root_payment_method').select('Reimbursement'); + /* cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.contains('Task: Enter NDR Items', { timeout: 60000 }); +*/ + // item 0 + cy.get('#root_item_0_sub_category').select('op_src'); + cy.get('#root_item_0_item_name') + .clear() + .type( + 'Open source software is code that is designed to be publicly accessible anyone can see, modify, END. This is now more than 100 characters' + ); + cy.get('#root_item_0_qty').clear().type('2'); + cy.get('#root_item_0_currency_type').select('Crypto'); + cy.get('#root_item_0_currency').select('SNT'); + cy.get('#root_item_0_unit_price').type('1915'); + + cy.get('#root_item > div:nth-child(3) > p > button').click(); + + // item 1 + cy.get('#root_item_1_sub_category').select('lic_and_sub'); + cy.get('#root_item_1_item_name') + .clear() + .type( + 'A software license is a document that provides legally binding guidelines for the use and distri END.' + ); + cy.get('#root_item_1_qty').clear().type('1'); + cy.get('#root_item_1_currency_type').select('Fiat'); + cy.get('#root_item_1_currency').select('AED'); + cy.get('#root_item_1_unit_price').type('4500'); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.contains( + 'Review and provide any supporting information or files for your request.', + { timeout: 60000 } + ); + + cy.get('.cds--text-area__wrapper') + .find('#root') + .type( + 'test 2021 Newest HP 17.3 inch FHD Laptop, AMD Ryzen 5 5500U 6core(Beat i7-1160G7, up to 4.0GHz),16GB RAM, 1TB PCIe SSD, Bluetooth 4.2, WiFi, HDMI, USB-A&C, Windows 10 S, w/Ghost Manta Accessories, Silver\nhttps://www.amazon.com/HP-i7-11G7-Bluetooth-Windows' + ); + + // cy.contains('Submit the Request').click(); + + // cy.get('input[value="Submit the Request"]').click(); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.get('button') + .contains(/^Return to Home$/) + .click(); + + cy.contains('Started by me', { timeout: 60000 }); + cy.logout(); + cy.wait(2000); + }); + }); + +}); + // Software and License - With Files describe('Initiate a Request - With Files', () => { Cypress._.times(1, () => { From ec7ecc17062a0dcefc8e6765a2f6dc712402d9a1 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 19 May 2023 10:18:14 -0400 Subject: [PATCH 18/60] edit documetation page --- docs/documentation/documentation.md | 70 ++++++++++++++++------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/docs/documentation/documentation.md b/docs/documentation/documentation.md index ccef5301..347da0c7 100644 --- a/docs/documentation/documentation.md +++ b/docs/documentation/documentation.md @@ -2,56 +2,61 @@ This documentation is currently hosted live at [Spiff-Arena's ReadTheDocs](https://spiff-arena.readthedocs.io/en/latest/) - Please set aside a couple of hours to work through this, as getting this setup correctly once is 10,000 times better than having problems every day for the rest of your life. ## Our Methodology -The methodology we are following is knowns as ["Docs as Code"](https://www.writethedocs.org/guide/docs-as-code/) +The methodology we are following is known as ["Docs as Code"](https://www.writethedocs.org/guide/docs-as-code/). -This means using the same tools and processes that software developers use for writing code to write the documenation for code. -In following this methodoloy, you will have to pick up some tools you haven't had to use before (Git, Sphinx). +This means using the same tools and processes that software developers use for writing code to write the documentation for code. +In following this methodology, you will have to pick up some tools you haven't had to use before (Git, Sphinx). Why would a technical writer need to learn these software engineering tools? I'll never make the case as well as an article by [Tom Johnson](https://idratherbewriting.com/trends/trends-to-follow-or-forget-docs-as-code.html). You might notice, when looking at the markdown files, that every sentence starts on a new line. Like this one. Unless there is a blank line between sentences, Markdown will still render this as a paragraph. -This is called [Ventilated Code](https://vanemden.wordpress.com/2009/01/01/ventilated-prose/) and can be very helpful when working in Markdown. +This is called [Ventilated Prose](https://vanemden.wordpress.com/2009/01/01/ventilated-prose/) and can be very helpful when working in Markdown. ## Our Tools -[Markdown](https://www.markdownguide.org/getting-started/) is a "markup language that you can use to add formatting elements to plain text documents. -You won't be writing the documentation in a word processor, but in simple plain text, and using some special syntax that will consistently and professionally format that text. +[Markdown](https://www.markdownguide.org/getting-started/) is a "markup language that you can use to add formatting elements to plain text documents." +You won't be writing the documentation in a word processor, but in simple plain text, and some special syntax that will consistently and professionally format that text. - -The basic Markdown syntax is very simple. Here are some [quick examples](https://commonmark.org/help/). And here is a great [10 minute tutorial](https://commonmark.org/help/tutorial/). +The basic Markdown syntax is very simple. +Here are some [quick examples](https://commonmark.org/help/). And here is a great [10 minute tutorial](https://commonmark.org/help/tutorial/). This will cover a lot of the basics, like bolding text, italics, paragraphs, lists and other common formatting techniques. ![Markdown screenshot](./images/markdown.png "Markdown example") ### MyST + Markdown doesn't support some really useful things. You can't add footnotes, or create an "aside" comment or build a table. -Because of this there are many extensions typically referened to as Markdown "Flavors". +Because of this, there are many extensions, and these are typically referred to as Markdown "Flavors." The flavor we are using is MyST. -There is [excellent documentation on MyST](https://myst-parser.readthedocs.io/en/v0.13.5/using/syntax.html) that you should definitely review, so you know everthing that is available to you. +There is [excellent documentation on MyST](https://myst-parser.readthedocs.io/en/v0.13.5/using/syntax.html) that you should definitely review, so you know everything that is available to you. ### Sphinx -This is a large documenation effort. Many different Markdown pages will together make up the full website. + +This is a large documentation effort. +Many different Markdown pages will together make up the full website. You will mostly use Sphinx in the background - you won't be aware of it. -But if you decide that you want to alter the theme (the colors, styles, etc...) of the final website, Sphinx controls this and offers [themes](https://sphinx-themes.org/) and the ability to change styles / colors and formatting through the site. +But if you decide that you want to alter the theme (the colors, styles, etc...) of the final website, Sphinx controls this and offers [themes](https://sphinx-themes.org/) and the ability to change styles / colors and formatting through the site. You just need to learn a little CSS to control it. ### GitHub + Our project is managed by a version control system called Git. -You can use GIT to submit changes to the documenation, in the same we use to submit changes to our code. -It is avilable on GitHub as the [spiff-arena project](https://github.com/sartography/spiff-arena). Git also manages versions of the code, and handles running tests, and causing our documenation to be built and deployed. -It will take a bit to get comfortable with Git, but when you do, you will come to love it (or maybe hate it, but with a lot of respect) +You can use GIT to submit changes to the documentation, in the same we use to submit changes to our code. +It is available on GitHub as the [spiff-arena project](https://github.com/sartography/spiff-arena). +GitHub also manages versions of the code and handles running tests. +Readthedocs observes changes in git and manages an automated process that causes our documentation to be built and deployed. +It will take a bit to get comfortable with Git, but when you do, you will come to love it (or maybe hate it, but with a lot of respect). ## Setup @@ -60,7 +65,7 @@ But you will find that most of it just works - and that once you get into a regu ### Step 1: Pre-Requisites -Assure you have been granted write access to our repository. +Assure you have been granted write access to our git repository. Make sure you have an account on GitHub and then contact dan@sartography.com and ask him to add you as a contributor. @@ -68,32 +73,34 @@ Make sure you have an account on GitHub and then contact dan@sartography.com and [Download VSCode](https://code.visualstudio.com/) and install it on your computer. ### Step 3: Install Python -We need python in order to build the website locally so we can really see what our content is going to look like once we publish. It's going to be handy for other reasons as well. We'll want python to be properly set up inside of VS Code. Follow [these directions and brief tutorial](https://code.visualstudio.com/docs/python/python-tutorial -) to assure this is set up. - +We need python in order to build the website locally so we can really see what our content is going to look like once we publish. +It's going to be handy for other reasons as well. +We'll want python to be properly set up inside of VS Code. +Follow [these directions and brief tutorial](https://code.visualstudio.com/docs/python/python-tutorial) to assure this is set up. ### Step 3: Connect VSCode to Git VSCode comes with Git built in. -So you can use VSCode to "pull" changes from others down to your local computer and "push" changes back up to share with others (and to cause our docs site to rebuild) +So you can use VSCode to "pull" changes from others down to your local computer and "push" changes back up to share with others (and to cause our docs site to rebuild). -Here are directions for how to [clone Spiff-Arena](https://learn.microsoft.com/en-us/azure/developer/javascript/how-to/with-visual-studio-code/clone-github-repository?tabs=create-repo-command-palette%2Cinitialize-repo-activity-bar%2Ccreate-branch-command-palette%2Ccommit-changes-command-palette%2Cpush-command-palette#clone-repository). **IMPORTANT**: Follow those directions, but be sure to checkout https://github.com/sartography/spiff-arena instead of the project they are using! +Here are directions for how to [clone Spiff-Arena](https://learn.microsoft.com/en-us/azure/developer/javascript/how-to/with-visual-studio-code/clone-github-repository?tabs=create-repo-command-palette%2Cinitialize-repo-activity-bar%2Ccreate-branch-command-palette%2Ccommit-changes-command-palette%2Cpush-command-palette#clone-repository). +**IMPORTANT**: Follow those directions, but be sure to checkout https://github.com/sartography/spiff-arena instead of the project they are using! You can save the project to any directory on your computer. We strongly suggest you create a sub-folder called "projects" in your "home" or "Desktop" folder and checkout the code into this directory. ### Step 4: Open just the Docs Folder -We've checked out the whole spiff-arena project, but we are only going to be working inside of the docs directory. So let's open just that folder in VSCode. +We've checked out the whole spiff-arena project, but we are only going to be working inside of the docs directory. +So let's open just that folder in VSCode. * Go to File -> Open Folder * Select the "docs" folder inside of spiff-arena. -Now clikc on the two pieces of paper at the top corner of your screen, and you should see a project that looks like this: +Now clikc on the two pieces of paper at the top corner of your screen, and you should see a project that looks like this without all the rest of the code in your way: ![Docs Directory](./images/docs_dir.png "Docs Directory") -Without all the rest of the code in your way. ### Step 4: Add some extensions * Inside VSCode, go to File -> Preferences -> Extensions @@ -105,7 +112,9 @@ Without all the rest of the code in your way. ### Step 5: Install Python Dependencies -This project requires a few Python dependencies to work correctly. We are going to set up a Virtual Evironment for Python to keep us sane later on. You can do that by following these steps: +This project requires a few Python dependencies to work correctly. +We are going to set up a Virtual Environment for Python to keep us sane later on. +You can do that by following these steps: 1. Open the Command Palette (Ctrl+Shift+P), start typing the **Python: Create Environment** command to search, and then select the command. 1. Select **Venv** @@ -114,18 +123,17 @@ This project requires a few Python dependencies to work correctly. We are goin 1. Click OK. ### Step 6: Fire up the website -1. Go to Terminial -> New Terminal +1. Go to Terminal -> New Terminal 1. type: **sphinx-autobuild . _build/html** at the prompt and hit enter. 1. Open your browser and go to http://127.0.0.1:8000 -### Step 7: Make a chance +### Step 7: Make a change 1. Open up a markdown file, and make a change. ### Step 8: Commit your changes and push them up for everyone. -1. Select the "git" button on the left hand side of the toolbar (cricles with lines between them) ![Git button](./images/git.png "Git button") +1. Select the "git" button on the left hand side of the toolbar (circles with lines between them) ![Git button](./images/git.png "Git button") 2. Press the blue "Commit" button. -3. Any changes you pushed up, should be live on our website within 5 to 10 minutes. - +3. Any changes you pushed up should be live on our website within 5 to 10 minutes. From 3f3b31f67b07f2185a39d9134146a2e0585f37d9 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 19 May 2023 10:30:57 -0400 Subject: [PATCH 19/60] debug github context --- .github/workflows/frontend_tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index 42a177da..abcce006 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -91,6 +91,11 @@ jobs: - name: wait_for_keycloak working-directory: ./spiffworkflow-backend run: ./keycloak/bin/wait_for_keycloak 5 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: | + echo "$GITHUB_CONTEXT" - name: Cypress run uses: cypress-io/github-action@v5 with: From a285037505fb2cf14b10830a913035990fabf66f Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 19 May 2023 10:50:55 -0400 Subject: [PATCH 20/60] added elevated permission macro --- .../src/spiffworkflow_backend/api.yml | 2 +- .../config/permissions/example_read_only.yml | 69 +++---------------- .../services/authorization_service.py | 33 +++++++-- .../unit/test_authorization_service.py | 28 ++++++++ 4 files changed, 66 insertions(+), 66 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 7dcb81ce..a4483f6b 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1122,7 +1122,7 @@ paths: - Process Instances responses: "200": - description: Empty ok true response on successful resume. + description: Empty ok true response on successful reset. content: application/json: schema: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml index b6facc64..10a22088 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml @@ -1,70 +1,17 @@ - groups: admin: users: [admin@spiffworkflow.org] permissions: - admin: + process-groups-ro: groups: [admin] allowed_permissions: [read] - uri: /* - - tasks-crud: + uri: PG:ALL + basic: groups: [admin] - allowed_permissions: [create, update, delete] - uri: /tasks/* - - process-instances-crud: - groups: [ admin ] - allowed_permissions: [create, update, delete] - uri: /process-instances/* - - suspend: + allowed_permissions: [ALL] + uri: BASIC + elevated-operations: groups: [admin] - allowed_permissions: [create] - uri: /v1.0/process-instance-suspend - - terminate: - groups: [admin] - allowed_permissions: [create] - uri: /v1.0/process-instance-terminate - - resume: - groups: [admin] - allowed_permissions: [create] - uri: /v1.0/process-instance-resume - - reset: - groups: [admin] - allowed_permissions: [create] - uri: /v1.0/process-instance-reset - - users-exist: - groups: [admin] - allowed_permissions: [create] - uri: /v1.0/users/exists/by-username - - send-event: - groups: [admin] - allowed_permissions: [create] - uri: /v1.0/send-event/* - - task-complete: - groups: [admin] - allowed_permissions: [create] - uri: /v1.0/task-complete/* - - messages: - groups: [admin] - allowed_permissions: [create] - uri: /v1.0/messages/* - - secrets: - groups: [admin] - allowed_permissions: [create, update, delete] - uri: /v1.0/secrets/* - - task-data: - groups: [admin] - allowed_permissions: [update] - uri: /v1.0/task-data/* + allowed_permissions: [ALL] + uri: ELEVATED diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 1ff58394..0f63531c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -438,10 +438,6 @@ class AuthorizationService: process_related_path_segment: str, target_uris: list[str], ) -> list[PermissionToAssign]: - permissions = permission_set.split(",") - if permission_set == "all": - permissions = ["create", "read", "update", "delete"] - permissions_to_assign: list[PermissionToAssign] = [] # we were thinking that if you can start an instance, you ought to be able to: @@ -472,7 +468,9 @@ class AuthorizationService: ]: permissions_to_assign.append(PermissionToAssign(permission="read", target_uri=target_uri)) else: + permissions = permission_set.split(",") if permission_set == "all": + permissions = ["create", "read", "update", "delete"] for path_segment_dict in PATH_SEGMENTS_FOR_PERMISSION_ALL: target_uri = f"{path_segment_dict['path']}/{process_related_path_segment}" relevant_permissions = path_segment_dict["relevant_permissions"] @@ -512,6 +510,29 @@ class AuthorizationService: permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/tasks/*")) return permissions_to_assign + @classmethod + def set_elevated_permissions(cls) -> list[PermissionToAssign]: + permissions_to_assign: list[PermissionToAssign] = [] + for process_instance_action in ["resume", "terminate", "suspend", "reset"]: + permissions_to_assign.append( + PermissionToAssign(permission="create", target_uri=f"/process-instances-{process_instance_action}/*") + ) + + # FIXME: we need to fix so that user that can start a process-model + # can also start through messages as well + permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/messages/*")) + + permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/send-event/*")) + permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*")) + + # read comes from PG and PM permissions + permissions_to_assign.append(PermissionToAssign(permission="update", target_uri="/task-data/*")) + + for permission in ["create", "read", "update", "delete"]: + permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/process-instances/*")) + permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/secrets/*")) + return permissions_to_assign + @classmethod def set_process_group_permissions(cls, target: str, permission_set: str) -> list[PermissionToAssign]: permissions_to_assign: list[PermissionToAssign] = [] @@ -557,6 +578,8 @@ class AuthorizationService: * affects given process-model BASIC * Basic access to complete tasks and use the site + ELEVATED + * Operations that require elevated permissions Permission Macros: all @@ -579,6 +602,8 @@ class AuthorizationService: elif target.startswith("BASIC"): permissions_to_assign += cls.set_basic_permissions() + elif target.startswith("ELEVATED"): + permissions_to_assign += cls.set_elevated_permissions() elif target == "ALL": for permission in permissions: permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/*")) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index a1f41d3e..a96b3017 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -301,6 +301,34 @@ class TestAuthorizationService(BaseTest): permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) assert permissions_to_assign_tuples == expected_permissions + def test_explode_permissions_elevated( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + expected_permissions = [ + ("/messages/*", "create"), + ("/process-instances-reset/*", "create"), + ("/process-instances-resume/*", "create"), + ("/process-instances-suspend/*", "create"), + ("/process-instances-terminate/*", "create"), + ("/process-instances/*", "create"), + ("/process-instances/*", "delete"), + ("/process-instances/*", "read"), + ("/process-instances/*", "update"), + ("/secrets/*", "create"), + ("/secrets/*", "delete"), + ("/secrets/*", "read"), + ("/secrets/*", "update"), + ("/send-event/*", "create"), + ("/task-complete/*", "create"), + ("/task-data/*", "update"), + ] + permissions_to_assign = AuthorizationService.explode_permissions("all", "ELEVATED") + permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) + assert permissions_to_assign_tuples == expected_permissions + def test_explode_permissions_all( self, app: Flask, From 0846f355900f7904b9d17708ba0fdbc0d2fa10c1 Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 19 May 2023 10:54:01 -0400 Subject: [PATCH 21/60] doc markdownlint --- docs/.markdownlint.jsonc | 5 +++++ docs/documentation/documentation.md | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 docs/.markdownlint.jsonc diff --git a/docs/.markdownlint.jsonc b/docs/.markdownlint.jsonc new file mode 100644 index 00000000..1095ce39 --- /dev/null +++ b/docs/.markdownlint.jsonc @@ -0,0 +1,5 @@ +{ + "default": true, + "MD013": false, + "whitespace": false +} diff --git a/docs/documentation/documentation.md b/docs/documentation/documentation.md index 347da0c7..6b0863cc 100644 --- a/docs/documentation/documentation.md +++ b/docs/documentation/documentation.md @@ -10,7 +10,7 @@ The methodology we are following is known as ["Docs as Code"](https://www.writet This means using the same tools and processes that software developers use for writing code to write the documentation for code. In following this methodology, you will have to pick up some tools you haven't had to use before (Git, Sphinx). -Why would a technical writer need to learn these software engineering tools? +Why would a technical writer need to learn these software engineering tools? I'll never make the case as well as an article by [Tom Johnson](https://idratherbewriting.com/trends/trends-to-follow-or-forget-docs-as-code.html). You might notice, when looking at the markdown files, that every sentence starts on a new line. @@ -137,3 +137,11 @@ You can do that by following these steps: 2. Press the blue "Commit" button. 3. Any changes you pushed up should be live on our website within 5 to 10 minutes. + +## Linting + +Documentation people: please ignore this for now. + +We may decide to check the documentation with a "linter" which is designed to keep the documentation consistent and standardized. +One option is https://github.com/igorshubovych/markdownlint-cli, which uses https://github.com/DavidAnson/markdownlint, which seems to be more popular than https://github.com/markdownlint/markdownlint. +A `.markdownlint.jsonc` file has been added that configures the same markdownlint program (basically to ignore the rule about long lines, since we are using ventilated prose). From d6829fcb6d9e447ecc4836b6fb7c4469d52bba4b Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 19 May 2023 11:23:44 -0400 Subject: [PATCH 22/60] merged in main and fixed acceptance tests --- spiffworkflow-frontend/cypress/support/commands.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-frontend/cypress/support/commands.js b/spiffworkflow-frontend/cypress/support/commands.js index 80383593..e7b6ca6d 100644 --- a/spiffworkflow-frontend/cypress/support/commands.js +++ b/spiffworkflow-frontend/cypress/support/commands.js @@ -1,4 +1,3 @@ -import { string } from 'prop-types'; import { modifyProcessIdentifierForPathParam } from '../../src/helpers'; import { miscDisplayName } from './helpers'; import 'cypress-file-upload'; @@ -115,7 +114,10 @@ Cypress.Commands.add( cy.contains('Task: ', { timeout: 30000 }); } else { cy.url().should('include', `/interstitial`); - cy.contains('Status: Completed'); + // cy.contains('Status: Completed'); + cy.contains( + 'There are no additional instructions or information for this task.' + ); if (returnToProcessModelShow) { cy.getBySel('process-model-breadcrumb-link').click(); cy.getBySel('process-model-show-permissions-loaded').should('exist'); From dda0c0f7179eff25b9ca6335235d1e47f0466c4c Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 19 May 2023 11:24:50 -0400 Subject: [PATCH 23/60] fixed failing cypress test --- spiffworkflow-frontend/cypress/support/commands.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spiffworkflow-frontend/cypress/support/commands.js b/spiffworkflow-frontend/cypress/support/commands.js index 80383593..e7b6ca6d 100644 --- a/spiffworkflow-frontend/cypress/support/commands.js +++ b/spiffworkflow-frontend/cypress/support/commands.js @@ -1,4 +1,3 @@ -import { string } from 'prop-types'; import { modifyProcessIdentifierForPathParam } from '../../src/helpers'; import { miscDisplayName } from './helpers'; import 'cypress-file-upload'; @@ -115,7 +114,10 @@ Cypress.Commands.add( cy.contains('Task: ', { timeout: 30000 }); } else { cy.url().should('include', `/interstitial`); - cy.contains('Status: Completed'); + // cy.contains('Status: Completed'); + cy.contains( + 'There are no additional instructions or information for this task.' + ); if (returnToProcessModelShow) { cy.getBySel('process-model-breadcrumb-link').click(); cy.getBySel('process-model-show-permissions-loaded').should('exist'); From efe532f75be7cad318a0a5a5044dc8e6a1e91d2c Mon Sep 17 00:00:00 2001 From: burnettk Date: Fri, 19 May 2023 11:33:56 -0400 Subject: [PATCH 24/60] record if the workflow run that triggered us was a push --- .github/workflows/frontend_tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index abcce006..4998fe8b 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -104,7 +104,11 @@ jobs: # only record on push, not pull_request, since we do not have secrets for PRs, # so the required CYPRESS_RECORD_KEY will not be available. # we have limited runs in cypress cloud, so only record main builds - record: ${{ github.ref_name == 'main' && github.event_name == 'push' }} + # the direct check for github.event_name == 'push' is for if we want to go back to triggering this workflow + # directly, rather than when Backend Tests complete. + # note that github.event.workflow_run is referring to the Backend Tests workflow and another option + # for github.event.workflow_run.event is 'pull_request', which we want to ignore. + record: ${{ github.ref_name == 'main' && ((github.event_name == 'workflow_run' && github.event.workflow_run.event == 'push') || (github.event_name == 'push')) }} env: # pass the Dashboard record key as an environment variable CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} From 83f7849685439cfbc7d2546d5c604dcb041b429d Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 19 May 2023 12:26:16 -0400 Subject: [PATCH 25/60] changed publish endpoint to precede model id so we can grant publish access but read only to a model otherwise w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 2 +- ...ml => example_process_model_read_only.yml} | 0 ...e_process_model_read_only_with_publish.yml | 21 +++++++++++++++++++ .../services/authorization_service.py | 6 ++++-- .../integration/test_process_api.py | 2 +- .../unit/test_authorization_service.py | 4 +++- .../src/hooks/UriListForPermissions.tsx | 2 +- .../src/routes/ProcessModelShow.tsx | 2 +- 8 files changed, 32 insertions(+), 7 deletions(-) rename spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/{example_read_only.yml => example_process_model_read_only.yml} (100%) create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index a4483f6b..41360853 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -564,7 +564,7 @@ paths: schema: $ref: "#/components/schemas/ProcessModel" - /process-models/{modified_process_model_identifier}/publish: + /process-model-publish/{modified_process_model_identifier}: parameters: - name: modified_process_model_identifier in: path diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only.yml similarity index 100% rename from spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml rename to spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only.yml diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml new file mode 100644 index 00000000..626dea4d --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml @@ -0,0 +1,21 @@ +groups: + admin: + users: [admin@spiffworkflow.org] + +permissions: + process-groups-ro: + groups: [admin] + allowed_permissions: [read] + uri: PG:ALL + basic: + groups: [admin] + allowed_permissions: [ALL] + uri: BASIC + elevated-operations: + groups: [admin] + allowed_permissions: [ALL] + uri: ELEVATED + process-model-publish: + groups: [admin] + allowed_permissions: [create] + uri: /process-model-publish/* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 0f63531c..79f43166 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -75,10 +75,11 @@ PATH_SEGMENTS_FOR_PERMISSION_ALL = [ "path": "/process-instances", "relevant_permissions": ["create", "read", "delete"], }, - {"path": "/process-instance-suspend", "relevant_permissions": ["create"]}, - {"path": "/process-instance-terminate", "relevant_permissions": ["create"]}, {"path": "/process-data", "relevant_permissions": ["read"]}, {"path": "/process-data-file-download", "relevant_permissions": ["read"]}, + {"path": "/process-instance-suspend", "relevant_permissions": ["create"]}, + {"path": "/process-instance-terminate", "relevant_permissions": ["create"]}, + {"path": "/process-model-publish", "relevant_permissions": ["create"]}, {"path": "/task-data", "relevant_permissions": ["read", "update"]}, ] @@ -524,6 +525,7 @@ class AuthorizationService: permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/send-event/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/users/search")) # read comes from PG and PM permissions permissions_to_assign.append(PermissionToAssign(permission="update", target_uri="/task-data/*")) 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 54d67848..533bd9a4 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -3033,7 +3033,7 @@ class TestProcessApi(BaseTest): # # # modified_process_model_id = process_model_identifier.replace("/", ":") # # response = client.post( - # # f"/v1.0/process-models/{modified_process_model_id}/publish?branch_to_update=staging", + # # f"/v1.0/process-model-publish/{modified_process_model_id}?branch_to_update=staging", # # headers=self.logged_in_headers(with_super_admin_user), # # ) # diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index a96b3017..928da5f1 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -141,6 +141,7 @@ class TestAuthorizationService(BaseTest): "delete", ), ("/process-instances/some-process-group:some-process-model:*", "read"), + ("/process-model-publish/some-process-group:some-process-model:*", "create"), ("/process-models/some-process-group:some-process-model:*", "create"), ("/process-models/some-process-group:some-process-model:*", "delete"), ("/process-models/some-process-group:some-process-model:*", "read"), @@ -194,7 +195,6 @@ class TestAuthorizationService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - """Test_explode_permissions_all_on_process_model.""" expected_permissions = sorted( [ ("/event-error-details/some-process-group:some-process-model/*", "read"), @@ -222,6 +222,7 @@ class TestAuthorizationService(BaseTest): "delete", ), ("/process-instances/some-process-group:some-process-model/*", "read"), + ("/process-model-publish/some-process-group:some-process-model/*", "create"), ("/process-models/some-process-group:some-process-model/*", "create"), ("/process-models/some-process-group:some-process-model/*", "delete"), ("/process-models/some-process-group:some-process-model/*", "read"), @@ -324,6 +325,7 @@ class TestAuthorizationService(BaseTest): ("/send-event/*", "create"), ("/task-complete/*", "create"), ("/task-data/*", "update"), + ("/users/search", "read"), ] permissions_to_assign = AuthorizationService.explode_permissions("all", "ELEVATED") permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) diff --git a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx index ae663541..e51d961d 100644 --- a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx +++ b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx @@ -27,7 +27,7 @@ export const useUriListForPermissions = () => { processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`, processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`, processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`, - processModelPublishPath: `/v1.0/process-models/${params.process_model_id}/publish`, + processModelPublishPath: `/v1.0/process-model-publish/${params.process_model_id}`, processModelShowPath: `/v1.0/process-models/${params.process_model_id}`, secretListPath: `/v1.0/secrets`, userSearch: `/v1.0/users/search`, diff --git a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx index 3b105dde..f50ee6a3 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx @@ -214,7 +214,7 @@ export default function ProcessModelShow() { setPublishDisabled(true); setProcessModelPublished(null); HttpService.makeCallToBackend({ - path: `/process-models/${modifiedProcessModelId}/publish`, + path: targetUris.processModelPublishPath, successCallback: postPublish, httpMethod: 'POST', }); From 4fcb0474a4a30a54a572564406558597f8ad1d10 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 19 May 2023 13:46:12 -0400 Subject: [PATCH 26/60] fixed example permission yml files --- .../config/permissions/example_process_model_read_only.yml | 4 ++-- .../example_process_model_read_only_with_publish.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only.yml index 10a22088..23324d17 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only.yml @@ -9,9 +9,9 @@ permissions: uri: PG:ALL basic: groups: [admin] - allowed_permissions: [ALL] + allowed_permissions: [all] uri: BASIC elevated-operations: groups: [admin] - allowed_permissions: [ALL] + allowed_permissions: [all] uri: ELEVATED diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml index 626dea4d..c50f7f77 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_process_model_read_only_with_publish.yml @@ -9,11 +9,11 @@ permissions: uri: PG:ALL basic: groups: [admin] - allowed_permissions: [ALL] + allowed_permissions: [all] uri: BASIC elevated-operations: groups: [admin] - allowed_permissions: [ALL] + allowed_permissions: [all] uri: ELEVATED process-model-publish: groups: [admin] From f862aad767ccbc5dee4cc707e7431a6208a8a840 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 19 May 2023 16:21:32 -0400 Subject: [PATCH 27/60] elevated permissions should include running privileged scripts w/ burnettk --- .../src/spiffworkflow_backend/services/authorization_service.py | 1 + .../spiffworkflow_backend/unit/test_authorization_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 79f43166..4063e0b6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -523,6 +523,7 @@ class AuthorizationService: # can also start through messages as well permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/messages/*")) + permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/can-run-privileged-script/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/send-event/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/users/search")) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 928da5f1..7f151329 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -309,6 +309,7 @@ class TestAuthorizationService(BaseTest): with_db_and_bpmn_file_cleanup: None, ) -> None: expected_permissions = [ + ("/can-run-privileged-script/*", "create"), ("/messages/*", "create"), ("/process-instances-reset/*", "create"), ("/process-instances-resume/*", "create"), From 289d8b44641c99be17e57ddb5d2632b3e13eabe3 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 19 May 2023 16:22:27 -0400 Subject: [PATCH 28/60] pyl w/ burnettk --- .../spiffworkflow_backend/services/authorization_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 4063e0b6..64814c29 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -523,7 +523,9 @@ class AuthorizationService: # can also start through messages as well permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/messages/*")) - permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/can-run-privileged-script/*")) + permissions_to_assign.append( + PermissionToAssign(permission="create", target_uri="/can-run-privileged-script/*") + ) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/send-event/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/users/search")) From 5f6ff7a87464402d75dd6ae507cf5098ca2ac36e Mon Sep 17 00:00:00 2001 From: burnettk Date: Sat, 20 May 2023 14:35:32 -0400 Subject: [PATCH 29/60] doc updates --- docs/documentation/documentation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/documentation/documentation.md b/docs/documentation/documentation.md index 6b0863cc..55353240 100644 --- a/docs/documentation/documentation.md +++ b/docs/documentation/documentation.md @@ -97,7 +97,7 @@ So let's open just that folder in VSCode. * Go to File -> Open Folder * Select the "docs" folder inside of spiff-arena. -Now clikc on the two pieces of paper at the top corner of your screen, and you should see a project that looks like this without all the rest of the code in your way: +Now click on the two pieces of paper at the top corner of your screen, and you should see a project that looks like this without all the rest of the code in your way: ![Docs Directory](./images/docs_dir.png "Docs Directory") @@ -106,7 +106,7 @@ Now clikc on the two pieces of paper at the top corner of your screen, and you s * Inside VSCode, go to File -> Preferences -> Extensions * Search for "myst" * click the "install" button. - * Repeat, this time doing it for "python extension for VS Code" + * Repeat, this time installing the "Python" extension for VS Code (from Microsoft) ![Myst Extension](./images/myst.png "Search or MyST in extensions") @@ -118,8 +118,8 @@ You can do that by following these steps: 1. Open the Command Palette (Ctrl+Shift+P), start typing the **Python: Create Environment** command to search, and then select the command. 1. Select **Venv** -1. Select Python 3.11 from the list of options if there is nore than one thing to select. -1. Be sure the the checkbox next to "requirements.txt" is selected. +1. Select Python 3.11 from the list of options if there is more than one thing to select. +1. Be sure the checkbox next to "requirements.txt" is selected. 1. Click OK. ### Step 6: Fire up the website From 881d5a08ba3350f5be0b2156e974b4d95f043626 Mon Sep 17 00:00:00 2001 From: burnettk Date: Sat, 20 May 2023 15:28:53 -0400 Subject: [PATCH 30/60] fix all markdownlint issues --- docs/documentation/documentation.md | 31 ++++++++++++++++++++--------- docs/quick_start/quick_start.md | 10 ++++++---- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/docs/documentation/documentation.md b/docs/documentation/documentation.md index 55353240..cd092b9e 100644 --- a/docs/documentation/documentation.md +++ b/docs/documentation/documentation.md @@ -65,14 +65,17 @@ But you will find that most of it just works - and that once you get into a regu ### Step 1: Pre-Requisites + Assure you have been granted write access to our git repository. -Make sure you have an account on GitHub and then contact dan@sartography.com and ask him to add you as a contributor. +Make sure you have an account on GitHub and then contact `dan@sartography.com` and ask him to add you as a contributor. ### Step 2: Install VSCode + [Download VSCode](https://code.visualstudio.com/) and install it on your computer. ### Step 3: Install Python + We need python in order to build the website locally so we can really see what our content is going to look like once we publish. It's going to be handy for other reasons as well. We'll want python to be properly set up inside of VS Code. @@ -80,11 +83,12 @@ Follow [these directions and brief tutorial](https://code.visualstudio.com/docs/ ### Step 3: Connect VSCode to Git + VSCode comes with Git built in. So you can use VSCode to "pull" changes from others down to your local computer and "push" changes back up to share with others (and to cause our docs site to rebuild). Here are directions for how to [clone Spiff-Arena](https://learn.microsoft.com/en-us/azure/developer/javascript/how-to/with-visual-studio-code/clone-github-repository?tabs=create-repo-command-palette%2Cinitialize-repo-activity-bar%2Ccreate-branch-command-palette%2Ccommit-changes-command-palette%2Cpush-command-palette#clone-repository). -**IMPORTANT**: Follow those directions, but be sure to checkout https://github.com/sartography/spiff-arena instead of the project they are using! +**IMPORTANT**: Follow those directions, but be sure to checkout `https://github.com/sartography/spiff-arena` instead of the project they are using! You can save the project to any directory on your computer. We strongly suggest you create a sub-folder called "projects" in your "home" or "Desktop" folder and checkout the code into this directory. @@ -103,15 +107,17 @@ Now click on the two pieces of paper at the top corner of your screen, and you s ### Step 4: Add some extensions - * Inside VSCode, go to File -> Preferences -> Extensions - * Search for "myst" - * click the "install" button. - * Repeat, this time installing the "Python" extension for VS Code (from Microsoft) + +* Inside VSCode, go to File -> Preferences -> Extensions +* Search for "myst" +* click the "install" button. +* Repeat, this time installing the "Python" extension for VS Code (from Microsoft) ![Myst Extension](./images/myst.png "Search or MyST in extensions") ### Step 5: Install Python Dependencies + This project requires a few Python dependencies to work correctly. We are going to set up a Virtual Environment for Python to keep us sane later on. You can do that by following these steps: @@ -123,15 +129,18 @@ You can do that by following these steps: 1. Click OK. ### Step 6: Fire up the website + 1. Go to Terminal -> New Terminal 1. type: **sphinx-autobuild . _build/html** at the prompt and hit enter. -1. Open your browser and go to http://127.0.0.1:8000 +1. Open your browser and go to [http://127.0.0.1:8000](http://127.0.0.1:8000). ### Step 7: Make a change + 1. Open up a markdown file, and make a change. -### Step 8: Commit your changes and push them up for everyone. +### Step 8: Commit your changes and push them up for everyone + 1. Select the "git" button on the left hand side of the toolbar (circles with lines between them) ![Git button](./images/git.png "Git button") 2. Press the blue "Commit" button. @@ -140,8 +149,12 @@ You can do that by following these steps: ## Linting +```{admonition} Linting is just an idea +:class: warning + Documentation people: please ignore this for now. +``` We may decide to check the documentation with a "linter" which is designed to keep the documentation consistent and standardized. -One option is https://github.com/igorshubovych/markdownlint-cli, which uses https://github.com/DavidAnson/markdownlint, which seems to be more popular than https://github.com/markdownlint/markdownlint. +One option is [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli), which uses David Anson's [NodeJS-based markdownlint](https://github.com/DavidAnson/markdownlint), which these days seems to be more popular than the [ruby-based markdownlint](https://github.com/markdownlint/markdownlint). A `.markdownlint.jsonc` file has been added that configures the same markdownlint program (basically to ignore the rule about long lines, since we are using ventilated prose). diff --git a/docs/quick_start/quick_start.md b/docs/quick_start/quick_start.md index d3ef647c..5f504f72 100644 --- a/docs/quick_start/quick_start.md +++ b/docs/quick_start/quick_start.md @@ -17,6 +17,7 @@ To access SpiffWorkflow, simply sign in using your Keycloak account. Once you ha :alt: Login Page :width: 45% ``` + ```{image} images/Untitled_1.png :alt: Home Page :width: 45% @@ -60,6 +61,7 @@ The process section provides a comprehensive view of the process ecosystem by sh :class: info 💡 A **process group** is a way of grouping a bunch of **process models.** A **process model** contains all the files necessary to execute a specific process. ``` + -- ![Untitled](images/Untitled_4.png) @@ -142,7 +144,7 @@ After starting a process, it's important to stay informed about its progress. Ev Here's how you can view the steps of the process you just started. -### Step 1: Navigate to the “Home” or “Process Instance” section. +### Step 1: Navigate to the “Home” or “Process Instance” section There are 2 ways of finding your process instances. @@ -170,7 +172,7 @@ The Process-defined **metadata can provide valuable insights into its history, c To check the metadata of a process instance, follow these steps. -### Step 1: Navigate to the “Home” or “Process Instance” section. +### Step 1: Navigate to the “Home” or “Process Instance” section as before Once you're signed in, navigate to the home section. Here you will find a list of all the process instances you've initiated under **“Started by me”**. @@ -231,7 +233,7 @@ To filter the list, click on the "Filter" option. This will expand the filter se ![Untitled](images/Untitled_20.png) -### Step 3: Apply Filters: +### Step 3: Apply Filters Once you have entered all the relevant filter details, click on the "**Apply**" button to apply the filters. The system will then display all the process instances matching the input details. @@ -303,4 +305,4 @@ Ensure that all required details have been included such as Process name, Proces ![Untitled](images/Untitled_32.png) -By following these steps, you can request the special permissions needed to carry out your tasks effectively. \ No newline at end of file +By following these steps, you can request the special permissions needed to carry out your tasks effectively. From d137af65e5b770812388dce4282da0d7b570a7ae Mon Sep 17 00:00:00 2001 From: burnettk Date: Sat, 20 May 2023 15:33:00 -0400 Subject: [PATCH 31/60] add lint target for make --- docs/Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb..5dd2db10 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -12,6 +12,9 @@ BUILDDIR = _build help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +lint: + markdownlint **/*.md + .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new From eb0414a66dc650c1fabcd47aa3c26df75ec92a0f Mon Sep 17 00:00:00 2001 From: burnettk Date: Sun, 21 May 2023 19:25:52 -0400 Subject: [PATCH 32/60] add make lint target --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 5dd2db10..d9148e85 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,7 +10,7 @@ BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) && echo " \033[0;34mlint\033[0m runs markdownlint on all markdown files (this was added to the Makefile manually. pardon formatting)" lint: markdownlint **/*.md From b8d06ae08e0aaf4428d49a8c1b8d412295bee856 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 22 May 2023 10:03:46 -0400 Subject: [PATCH 33/60] fixed process-instance api perms in macro --- .../services/authorization_service.py | 2 +- .../unit/test_authorization_service.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 64814c29..bc558dc4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -516,7 +516,7 @@ class AuthorizationService: permissions_to_assign: list[PermissionToAssign] = [] for process_instance_action in ["resume", "terminate", "suspend", "reset"]: permissions_to_assign.append( - PermissionToAssign(permission="create", target_uri=f"/process-instances-{process_instance_action}/*") + PermissionToAssign(permission="create", target_uri=f"/process-instance-{process_instance_action}/*") ) # FIXME: we need to fix so that user that can start a process-model diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 7f151329..114fb476 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -311,10 +311,10 @@ class TestAuthorizationService(BaseTest): expected_permissions = [ ("/can-run-privileged-script/*", "create"), ("/messages/*", "create"), - ("/process-instances-reset/*", "create"), - ("/process-instances-resume/*", "create"), - ("/process-instances-suspend/*", "create"), - ("/process-instances-terminate/*", "create"), + ("/process-instance-reset/*", "create"), + ("/process-instance-resume/*", "create"), + ("/process-instance-suspend/*", "create"), + ("/process-instance-terminate/*", "create"), ("/process-instances/*", "create"), ("/process-instances/*", "delete"), ("/process-instances/*", "read"), From e63ddd301524013c98d247d108d3eddf6c505a5c Mon Sep 17 00:00:00 2001 From: Kevin Burnett <18027+burnettk@users.noreply.github.com> Date: Mon, 22 May 2023 09:31:08 -0700 Subject: [PATCH 34/60] update setup steps --- README.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 66a3b3c3..b2806a41 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,23 @@ If you need to push back from the monorepo to one of the individual repos, here' git subtree push --prefix=spiffworkflow-frontend git@github.com:sartography/spiffworkflow-frontend.git add_md_file -Setup ------ +## Backend Setup +First install python and poetry, and then: + + cd spiffworkflow-backend poetry install + ./bin/run_server_locally -Run tests ---------- +## Frontend Setup +First install nodejs, ideally the version in .tool-versions (but likely other versions will work). Then: + + cd spiffworkflow-frontend + npm install + npm start + +## Run tests ./bin/run_pyl Requires at root: @@ -31,26 +40,19 @@ Requires at root: - .pre-commit-config.yaml - pyproject.toml -Run cypress automated browser tests ------------------------------------ +## Run cypress automated browser tests -Get the app running so you can access the frontend at http://localhost:7001 in your browser. +Get the app running so you can access the frontend at http://localhost:7001 in your browser by following the frontend and backend setup steps above, and then: -First install nodejs, ideally the version in .tool-versions (but likely other versions will work). - -Then: - - cd spiffworkflow-frontend - npm install ./bin/run_cypress_tests_locally -License -------- +## License + SpiffArena's main components are published under the terms of the [GNU Lesser General Public License (LGPL) Version 3](https://www.gnu.org/licenses/lgpl-3.0.txt). -Support -------- +## Support + You can find us on [our Discord Channel](https://discord.gg/BYHcc7PpUC). Commercial support for SpiffWorkflow is available from [Sartography](https://sartography.com). From a3890afba15f6729d1cb3843243895fa92d35bba Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 22 May 2023 13:50:32 -0400 Subject: [PATCH 35/60] added missing permissions to elevated perm macro and removed api calls from unit tests --- spiffworkflow-backend/conftest.py | 64 +------- .../src/spiffworkflow_backend/api.yml | 6 +- .../config/permissions/unit_testing.yml | 14 +- .../routes/tasks_controller.py | 2 +- .../services/authorization_service.py | 24 ++- .../services/process_instance_processor.py | 4 +- .../helpers/base_test.py | 76 +++++++++- .../integration/test_debug_controller.py | 2 +- .../integration/test_messages.py | 63 ++++++++ .../unit/test_authorization_service.py | 42 +++--- .../unit/test_dot_notation.py | 23 +-- .../unit/test_error_handling_service.py | 14 +- .../unit/test_message_instance.py | 42 ++---- .../unit/test_message_service.py | 142 +----------------- .../unit/test_permissions.py | 10 -- .../unit/test_process_instance_processor.py | 40 ++--- .../unit/test_process_instance_report.py | 141 ----------------- .../unit/test_process_instance_service.py | 10 -- .../unit/test_restricted_script_engine.py | 4 - .../unit/test_script_unit_test_runner.py | 18 --- .../unit/test_spec_file_service.py | 64 +++----- .../unit/test_various_bpmn_constructs.py | 16 +- .../src/components/ActiveUsers.tsx | 2 + 23 files changed, 265 insertions(+), 558 deletions(-) create mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py delete mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index 45e9fd54..7006c17a 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -62,65 +62,5 @@ def with_db_and_bpmn_file_cleanup() -> None: @pytest.fixture() def with_super_admin_user() -> UserModel: - """With_super_admin_user.""" - return BaseTest.create_user_with_permission("super_admin") - - -@pytest.fixture() -def setup_process_instances_for_reports( - client: FlaskClient, with_super_admin_user: UserModel -) -> list[ProcessInstanceModel]: - """Setup_process_instances_for_reports.""" - user = with_super_admin_user - process_group_id = "runs_without_input" - process_model_id = "sample" - # bpmn_file_name = "sample.bpmn" - bpmn_file_location = "sample" - process_model_identifier = BaseTest().create_group_and_model_with_bpmn( - client, - with_super_admin_user, - process_group_id=process_group_id, - process_model_id=process_model_id, - # bpmn_file_name=bpmn_file_name, - bpmn_file_location=bpmn_file_location, - ) - - # BaseTest().create_process_group( - # client=client, user=user, process_group_id=process_group_id, display_name=process_group_id - # ) - # process_model_id = "runs_without_input/sample" - # load_test_spec( - # process_model_id=f"{process_group_id}/{process_model_id}", - # process_model_source_directory="sample" - # ) - process_instances = [] - for data in [kay(), ray(), jay()]: - process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( - # process_group_identifier=process_group_id, - process_model_identifier=process_model_identifier, - user=user, - ) - processor = ProcessInstanceProcessor(process_instance) - processor.slam_in_data(data) - process_instance.status = "complete" - db.session.add(process_instance) - db.session.commit() - - process_instances.append(process_instance) - - return process_instances - - -def kay() -> dict: - """Kay.""" - return {"name": "kay", "grade_level": 2, "test_score": 10} - - -def ray() -> dict: - """Ray.""" - return {"name": "ray", "grade_level": 1, "test_score": 9} - - -def jay() -> dict: - """Jay.""" - return {"name": "jay", "grade_level": 2, "test_score": 8} + raise Exception("HEY") + # return BaseTest.create_user_with_permission("super_admin") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 41360853..8eda4b9f 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -149,7 +149,7 @@ paths: $ref: "#/components/schemas/OkTrue" /debug/test-raise-error: - get: + post: operationId: spiffworkflow_backend.routes.debug_controller.test_raise_error summary: Returns an unhandled exception that should notify sentry, if sentry is configured tags: @@ -184,7 +184,7 @@ paths: description: The identifier for the last visited page for the user. schema: type: string - get: + post: tags: - Active User operationId: spiffworkflow_backend.routes.active_users_controller.active_user_updates @@ -207,7 +207,7 @@ paths: description: The identifier for the last visited page for the user. schema: type: string - get: + post: tags: - Active User operationId: spiffworkflow_backend.routes.active_users_controller.active_user_unregister diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml index 10980462..4192d1dd 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/unit_testing.yml @@ -16,10 +16,18 @@ groups: users: [testuser2, testuser3, testuser4] permissions: - admin: + process-groups-all: groups: [admin] - allowed_permissions: [create, read, update, delete] - uri: /* + allowed_permissions: [all] + uri: PG:ALL + basic: + groups: [admin] + allowed_permissions: [all] + uri: BASIC + elevated-operations: + groups: [admin] + allowed_permissions: [all] + uri: ELEVATED read-all: groups: ["Finance Team", hr, admin] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 3af7a412..2b68942d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -257,7 +257,7 @@ def manual_complete_task( process_instance = ProcessInstanceModel.query.filter(ProcessInstanceModel.id == int(process_instance_id)).first() if process_instance: processor = ProcessInstanceProcessor(process_instance) - processor.manual_complete_task(task_guid, execute) + processor.manual_complete_task(task_guid, execute, g.user) else: raise ApiError( error_code="complete_task", diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index bc558dc4..322dedd6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -79,6 +79,7 @@ PATH_SEGMENTS_FOR_PERMISSION_ALL = [ {"path": "/process-data-file-download", "relevant_permissions": ["read"]}, {"path": "/process-instance-suspend", "relevant_permissions": ["create"]}, {"path": "/process-instance-terminate", "relevant_permissions": ["create"]}, + {"path": "/process-model-natural-language", "relevant_permissions": ["create"]}, {"path": "/process-model-publish", "relevant_permissions": ["create"]}, {"path": "/task-data", "relevant_permissions": ["read", "update"]}, ] @@ -487,11 +488,10 @@ class AuthorizationService: @classmethod def set_basic_permissions(cls) -> list[PermissionToAssign]: permissions_to_assign: list[PermissionToAssign] = [] + permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/active-users/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/process-instances/for-me")) - permissions_to_assign.append( - PermissionToAssign(permission="read", target_uri="/process-instances/report-metadata") - ) - permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/active-users/*")) + permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/users/exists/by-username")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/connector-proxy/typeahead/*")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/debug/version-info")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-groups")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-models")) @@ -499,7 +499,11 @@ class AuthorizationService: permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/processes/callers")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/service-tasks")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/user-groups/for-current-user")) - permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/users/exists/by-username")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/users/search")) + + permissions_to_assign.append( + PermissionToAssign(permission="read", target_uri="/process-instances/report-metadata") + ) permissions_to_assign.append( PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*") ) @@ -522,13 +526,15 @@ class AuthorizationService: # FIXME: we need to fix so that user that can start a process-model # can also start through messages as well permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/messages/*")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/messages")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/authentications")) permissions_to_assign.append( PermissionToAssign(permission="create", target_uri="/can-run-privileged-script/*") ) + permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/debug/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/send-event/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/task-complete/*")) - permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/users/search")) # read comes from PG and PM permissions permissions_to_assign.append(PermissionToAssign(permission="update", target_uri="/task-data/*")) @@ -697,6 +703,8 @@ class AuthorizationService: group_identifier = group["name"] GroupService.find_or_create_group(group_identifier) for username in group["users"]: + if user_model and username != user_model.username: + continue user_to_group_dict: UserToGroupDict = { "username": username, "group_identifier": group_identifier, @@ -704,6 +712,10 @@ class AuthorizationService: user_to_group_identifiers.append(user_to_group_dict) GroupService.add_user_to_group_or_add_to_waiting(username, group_identifier) unique_user_group_identifiers.add(group_identifier) + for group in group_permissions: + group_identifier = group["name"] + if user_model and group_identifier not in unique_user_group_identifiers: + continue for permission in group["permissions"]: for crud_op in permission["actions"]: permission_assignments.extend( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index b64cedfd..60d160c6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -1109,7 +1109,7 @@ class ProcessInstanceProcessor: # TODO: do_engine_steps without a lock self.do_engine_steps(save=True) - def manual_complete_task(self, task_id: str, execute: bool) -> None: + def manual_complete_task(self, task_id: str, execute: bool, user: UserModel) -> None: """Mark the task complete optionally executing it.""" spiff_task = self.bpmn_process_instance.get_task_from_id(UUID(task_id)) event_type = ProcessInstanceEventType.task_skipped.value @@ -1122,7 +1122,7 @@ class ProcessInstanceProcessor: f" instance {self.process_instance_model.id}" ) human_task = HumanTaskModel.query.filter_by(task_id=task_id).first() - self.complete_task(spiff_task, human_task=human_task, user=g.user) + self.complete_task(spiff_task, human_task=human_task, user=user) elif execute: current_app.logger.info( f"Manually executing Task {spiff_task.task_spec.name} of process" diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 3a5290ed..499567cf 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -9,9 +9,12 @@ from typing import Optional from flask import current_app from flask.testing import FlaskClient +from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor +from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from werkzeug.test import TestResponse # type: ignore +from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.permission_assignment import Permission @@ -314,9 +317,11 @@ class BaseTest: target_uri: str = PermissionTargetModel.URI_ALL, permission_names: Optional[list[str]] = None, ) -> UserModel: - """Create_user_with_permission.""" - user = BaseTest.find_or_create_user(username=username) - return cls.add_permissions_to_user(user, target_uri=target_uri, permission_names=permission_names) + # user = BaseTest.find_or_create_user(username=username) + # return cls.add_permissions_to_user(user, target_uri=target_uri, permission_names=permission_names) + user = BaseTest.find_or_create_user(username='testadmin1') + AuthorizationService.import_permissions_from_yaml_file(user) + return user @classmethod def add_permissions_to_user( @@ -325,7 +330,6 @@ class BaseTest: target_uri: str = PermissionTargetModel.URI_ALL, permission_names: Optional[list[str]] = None, ) -> UserModel: - """Add_permissions_to_user.""" permission_target = AuthorizationService.find_or_create_permission_target(target_uri) if permission_names is None: @@ -401,3 +405,67 @@ class BaseTest: def empty_report_metadata_body(self) -> ReportMetadata: return {"filter_by": [], "columns": [], "order_by": []} + + def start_sender_process( + self, + client: FlaskClient, + payload: dict, + group_name: str = "test_group", + ) -> ProcessInstanceModel: + process_model = load_test_spec( + "test_group/message", + process_model_source_directory="message_send_one_conversation", + bpmn_file_name="message_sender.bpmn", # Slightly misnamed, it sends and receives + ) + + process_instance = self.create_process_instance_from_process_model( + process_model + ) + processor_send_receive = ProcessInstanceProcessor(process_instance) + processor_send_receive.do_engine_steps(save=True) + task = processor_send_receive.get_all_user_tasks()[0] + human_task = process_instance.active_human_tasks[0] + + ProcessInstanceService.complete_form_task( + processor_send_receive, + task, + payload, + process_instance.process_initiator, + human_task, + ) + processor_send_receive.save() + return process_instance + + def assure_a_message_was_sent(self, process_instance: ProcessInstanceModel, payload: dict) -> None: + # There should be one new send message for the given process instance. + send_messages = ( + MessageInstanceModel.query.filter_by(message_type="send") + .filter_by(process_instance_id=process_instance.id) + .order_by(MessageInstanceModel.id) + .all() + ) + assert len(send_messages) == 1 + send_message = send_messages[0] + assert send_message.payload == payload, "The send message should match up with the payload" + assert send_message.name == "Request Approval" + assert send_message.status == "ready" + + def assure_there_is_a_process_waiting_on_a_message(self, process_instance: ProcessInstanceModel) -> None: + # There should be one new send message for the given process instance. + waiting_messages = ( + MessageInstanceModel.query.filter_by(message_type="receive") + .filter_by(status="ready") + .filter_by(process_instance_id=process_instance.id) + .order_by(MessageInstanceModel.id) + .all() + ) + assert len(waiting_messages) == 1 + waiting_message = waiting_messages[0] + self.assure_correlation_properties_are_right(waiting_message) + + def assure_correlation_properties_are_right(self, message: MessageInstanceModel) -> None: + # Correlation Properties should match up + po_curr = next(c for c in message.correlation_rules if c.name == "po_number") + customer_curr = next(c for c in message.correlation_rules if c.name == "customer_id") + assert po_curr is not None + assert customer_curr is not None diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py index d9136020..ce19a7bf 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_debug_controller.py @@ -10,7 +10,7 @@ class TestDebugController(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - response = client.get( + response = client.post( "/v1.0/debug/test-raise-error", ) assert response.status_code == 500 diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py new file mode 100644 index 00000000..187a7813 --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py @@ -0,0 +1,63 @@ +import pytest +from flask import Flask +from flask import g +from flask.testing import FlaskClient +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + +from spiffworkflow_backend.exceptions.api_error import ApiError +from spiffworkflow_backend.models.message_instance import MessageInstanceModel +from spiffworkflow_backend.routes.messages_controller import message_send + + +class TestMessages(BaseTest): + def test_message_from_api_into_running_process( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + """Test sending a message to a running process via the API. + + This example workflow will send a message called 'request_approval' and then wait for a response message + of 'approval_result'. This test assures that it will fire the message with the correct correlation properties + and will respond only to a message called 'approval_result' that has the matching correlation properties, + as sent by an API Call. + """ + payload = { + "customer_id": "Sartography", + "po_number": 1001, + "description": "We built a new feature for messages!", + "amount": "100.00", + } + process_instance = self.start_sender_process(client, payload, "test_from_api") + self.assure_a_message_was_sent(process_instance, payload) + self.assure_there_is_a_process_waiting_on_a_message(process_instance) + g.user = process_instance.process_initiator + + # Make an API call to the service endpoint, but use the wrong po number + with pytest.raises(ApiError): + message_send("Approval Result", {"payload": {"po_number": 5001}}) + + # Should return an error when making an API call for right po number, wrong client + with pytest.raises(ApiError): + message_send( + "Approval Result", + {"payload": {"po_number": 1001, "customer_id": "jon"}}, + ) + + # No error when calling with the correct parameters + message_send( + "Approval Result", + {"payload": {"po_number": 1001, "customer_id": "Sartography"}}, + ) + + # There is no longer a waiting message + waiting_messages = ( + MessageInstanceModel.query.filter_by(message_type="receive") + .filter_by(status="ready") + .filter_by(process_instance_id=process_instance.id) + .all() + ) + assert len(waiting_messages) == 0 + # The process has completed + assert process_instance.status == "complete" diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 114fb476..514b2cea 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -1,5 +1,6 @@ """Test_message_service.""" import pytest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest @@ -60,7 +61,6 @@ class TestAuthorizationService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_user_can_be_added_to_human_task_on_first_login.""" initiator_user = self.find_or_create_user("initiator_user") @@ -69,16 +69,12 @@ class TestAuthorizationService(BaseTest): self.find_or_create_user("testuser1") AuthorizationService.import_permissions_from_yaml_file() - process_model_identifier = self.create_group_and_model_with_bpmn( - client=client, - user=with_super_admin_user, - process_group_id="test_group", - process_model_id="model_with_lanes", + process_model = load_test_spec( + process_model_id="test_group/model_with_lanes", bpmn_file_name="lanes.bpmn", - bpmn_file_location="model_with_lanes", + process_model_source_directory="model_with_lanes", ) - process_model = ProcessModelService.get_process_model(process_model_id=process_model_identifier) process_instance = self.create_process_instance_from_process_model( process_model=process_model, user=initiator_user ) @@ -141,6 +137,7 @@ class TestAuthorizationService(BaseTest): "delete", ), ("/process-instances/some-process-group:some-process-model:*", "read"), + ('/process-model-natural-language/some-process-group:some-process-model:*', "create"), ("/process-model-publish/some-process-group:some-process-model:*", "create"), ("/process-models/some-process-group:some-process-model:*", "create"), ("/process-models/some-process-group:some-process-model:*", "delete"), @@ -163,7 +160,7 @@ class TestAuthorizationService(BaseTest): with_db_and_bpmn_file_cleanup: None, ) -> None: """Test_explode_permissions_start_on_process_group.""" - expected_permissions = [ + expected_permissions = sorted([ ("/event-error-details/some-process-group:some-process-model:*", "read"), ( "/logs/some-process-group:some-process-model:*", @@ -182,7 +179,7 @@ class TestAuthorizationService(BaseTest): "read", ), ("/process-instances/some-process-group:some-process-model:*", "create"), - ] + ]) permissions_to_assign = AuthorizationService.explode_permissions( "start", "PG:/some-process-group/some-process-model" ) @@ -222,6 +219,7 @@ class TestAuthorizationService(BaseTest): "delete", ), ("/process-instances/some-process-group:some-process-model/*", "read"), + ('/process-model-natural-language/some-process-group:some-process-model/*', "create"), ("/process-model-publish/some-process-group:some-process-model/*", "create"), ("/process-models/some-process-group:some-process-model/*", "create"), ("/process-models/some-process-group:some-process-model/*", "delete"), @@ -244,7 +242,7 @@ class TestAuthorizationService(BaseTest): with_db_and_bpmn_file_cleanup: None, ) -> None: """Test_explode_permissions_start_on_process_model.""" - expected_permissions = [ + expected_permissions = sorted([ ( "/event-error-details/some-process-group:some-process-model/*", "read", @@ -263,7 +261,7 @@ class TestAuthorizationService(BaseTest): "read", ), ("/process-instances/some-process-group:some-process-model/*", "create"), - ] + ]) permissions_to_assign = AuthorizationService.explode_permissions( "start", "PM:/some-process-group/some-process-model" ) @@ -276,8 +274,9 @@ class TestAuthorizationService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - expected_permissions = [ - ("/active-users/*", "read"), + expected_permissions = sorted([ + ("/active-users/*", "create"), + ("/connector-proxy/typeahead/*", "read"), ("/debug/version-info", "read"), ("/process-groups", "read"), ("/process-instances/find-by-id/*", "read"), @@ -297,7 +296,8 @@ class TestAuthorizationService(BaseTest): ("/tasks/*", "update"), ("/user-groups/for-current-user", "read"), ("/users/exists/by-username", "create"), - ] + ("/users/search", "read"), + ]) permissions_to_assign = AuthorizationService.explode_permissions("all", "BASIC") permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) assert permissions_to_assign_tuples == expected_permissions @@ -308,8 +308,11 @@ class TestAuthorizationService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - expected_permissions = [ + expected_permissions = sorted([ + ("/authentications", "read"), ("/can-run-privileged-script/*", "create"), + ("/debug/*", "create"), + ("/messages", "read"), ("/messages/*", "create"), ("/process-instance-reset/*", "create"), ("/process-instance-resume/*", "create"), @@ -326,8 +329,7 @@ class TestAuthorizationService(BaseTest): ("/send-event/*", "create"), ("/task-complete/*", "create"), ("/task-data/*", "update"), - ("/users/search", "read"), - ] + ]) permissions_to_assign = AuthorizationService.explode_permissions("all", "ELEVATED") permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) assert permissions_to_assign_tuples == expected_permissions @@ -437,7 +439,7 @@ class TestAuthorizationService(BaseTest): AuthorizationService.refresh_permissions(group_info) assert GroupModel.query.filter_by(identifier="group_two").first() is None assert GroupModel.query.filter_by(identifier="group_one").first() is not None - self.assert_user_has_permission(admin_user, "create", "/anything-they-want") + self.assert_user_has_permission(admin_user, "create", "/v1.0/process-groups/whatever") self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey") self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo") self.assert_user_has_permission(user, "create", "/v1.0/process-groups/hey:yo") @@ -469,7 +471,7 @@ class TestAuthorizationService(BaseTest): self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey") self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo") self.assert_user_has_permission(user, "create", "/v1.0/process-groups/hey:yo", expected_result=False) - self.assert_user_has_permission(admin_user, "create", "/anything-they-want") + self.assert_user_has_permission(admin_user, "create", "/v1.0/process-groups/whatever") self.assert_user_has_permission(user_two, "read", "/v1.0/process-groups/hey", expected_result=False) assert GroupModel.query.filter_by(identifier="group_three").first() is not None diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py index ca05db9c..2a4ad999 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py @@ -1,5 +1,6 @@ """Test_various_bpmn_constructs.""" from flask.app import Flask +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest @@ -18,29 +19,17 @@ class TestDotNotation(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_form_data_conversion_to_dot_dict.""" - process_group_id = "dot_notation_group" - process_model_id = "test_dot_notation" + process_model_id = "dot_notation_group/test_dot_notation" bpmn_file_name = "diagram.bpmn" bpmn_file_location = "dot_notation" - process_model_identifier = self.create_group_and_model_with_bpmn( - client, - with_super_admin_user, - process_group_id=process_group_id, + process_model = load_test_spec( process_model_id=process_model_id, bpmn_file_name=bpmn_file_name, - bpmn_file_location=bpmn_file_location, + process_model_source_directory=bpmn_file_location, ) - headers = self.logged_in_headers(with_super_admin_user) - response = self.create_process_instance_from_process_model_id_with_api( - client, process_model_identifier, headers - ) - process_instance_id = response.json["id"] - process_instance = ProcessInstanceService().get_process_instance(process_instance_id) - + process_instance = self.create_process_instance_from_process_model(process_model) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True) human_task = process_instance.human_tasks[0] @@ -53,7 +42,7 @@ class TestDotNotation(BaseTest): "invoice.invoiceAmount": "1000.00", "invoice.dueDate": "09/30/2022", } - ProcessInstanceService.complete_form_task(processor, user_task, form_data, with_super_admin_user, human_task) + ProcessInstanceService.complete_form_task(processor, user_task, form_data, process_instance.process_initiator, human_task) expected = { "contibutorName": "Elizabeth", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py index 33eb86cc..6c3d893b 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py @@ -28,10 +28,10 @@ class TestErrorHandlingService(BaseTest): """ def run_process_model_and_handle_error( - self, process_model: ProcessModelInfo, user: UserModel + self, process_model: ProcessModelInfo ) -> ProcessInstanceModel: - process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( - process_model.id, user + process_instance = self.create_process_instance_from_process_model( + process_model ) pip = ProcessInstanceProcessor(process_instance) with pytest.raises(WorkflowExecutionServiceError) as e: @@ -44,7 +44,6 @@ class TestErrorHandlingService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Process Model in DB marked as suspended when error occurs.""" process_model = load_test_spec( @@ -54,13 +53,13 @@ class TestErrorHandlingService(BaseTest): ) # Process instance should be marked as errored by default. - process_instance = self.run_process_model_and_handle_error(process_model, with_super_admin_user) + process_instance = self.run_process_model_and_handle_error(process_model) assert ProcessInstanceStatus.error.value == process_instance.status # If process model should be suspended on error, then that is what should happen. process_model.fault_or_suspend_on_exception = "suspend" ProcessModelService.save_process_model(process_model) - process_instance = self.run_process_model_and_handle_error(process_model, with_super_admin_user) + process_instance = self.run_process_model_and_handle_error(process_model) assert ProcessInstanceStatus.suspended.value == process_instance.status def test_error_sends_bpmn_message( @@ -68,7 +67,6 @@ class TestErrorHandlingService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Real BPMN Messages should get generated and processes should fire off and complete.""" process_model = load_test_spec( @@ -85,7 +83,7 @@ class TestErrorHandlingService(BaseTest): process_model.exception_notification_addresses = ["dan@ILoveToReadErrorsInMyEmails.com"] ProcessModelService.save_process_model(process_model) # kick off the process and assure it got marked as an error. - process_instance = self.run_process_model_and_handle_error(process_model, with_super_admin_user) + process_instance = self.run_process_model_and_handle_error(process_model) assert ProcessInstanceStatus.error.value == process_instance.status # Both send and receive messages should be generated, matched diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py index 2c07222e..9f651d76 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py @@ -1,7 +1,9 @@ """Test_message_instance.""" import pytest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask import Flask from flask.testing import FlaskClient +from spiffworkflow_backend.models.process_model import ProcessModelInfo from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.db import db @@ -11,36 +13,25 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer class TestMessageInstance(BaseTest): - """TestMessageInstance.""" - - def setup_message_tests(self, client: FlaskClient, user: UserModel) -> str: - """Setup_message_tests.""" - process_group_id = "test_group" - process_model_id = "hello_world" + def setup_message_tests(self, client: FlaskClient) -> ProcessModelInfo: + process_model_id = "testk_group/hello_world" bpmn_file_name = "hello_world.bpmn" bpmn_file_location = "hello_world" - process_model_identifier = self.create_group_and_model_with_bpmn( - client, - user, - process_group_id=process_group_id, + process_model = load_test_spec( process_model_id=process_model_id, bpmn_file_name=bpmn_file_name, - bpmn_file_location=bpmn_file_location, + process_model_source_directory=bpmn_file_location, ) - return process_model_identifier + return process_model def test_can_create_message_instance( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_can_create_message_instance.""" message_name = "Message Model One" - process_model_identifier = self.setup_message_tests(client, with_super_admin_user) - - process_model = ProcessModelService.get_process_model(process_model_id=process_model_identifier) + process_model = self.setup_message_tests(client) process_instance = self.create_process_instance_from_process_model(process_model, "waiting") queued_message = MessageInstanceModel( @@ -64,12 +55,9 @@ class TestMessageInstance(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_cannot_set_invalid_status.""" message_name = "message_model_one" - process_model_identifier = self.setup_message_tests(client, with_super_admin_user) - process_model = ProcessModelService.get_process_model(process_model_id=process_model_identifier) + process_model = self.setup_message_tests(client) process_instance = self.create_process_instance_from_process_model(process_model, "waiting") with pytest.raises(ValueError) as exception: @@ -100,13 +88,9 @@ class TestMessageInstance(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_cannot_set_invalid_message_type.""" message_name = "message_model_one" - process_model_identifier = self.setup_message_tests(client, with_super_admin_user) - - process_model = ProcessModelService.get_process_model(process_model_id=process_model_identifier) + process_model = self.setup_message_tests(client) process_instance = self.create_process_instance_from_process_model(process_model, "waiting") with pytest.raises(ValueError) as exception: @@ -136,13 +120,9 @@ class TestMessageInstance(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_force_failure_cause_if_status_is_failure.""" message_name = "message_model_one" - process_model_identifier = self.setup_message_tests(client, with_super_admin_user) - - process_model = ProcessModelService.get_process_model(process_model_id=process_model_identifier) + process_model = self.setup_message_tests(client) process_instance = self.create_process_instance_from_process_model(process_model, "waiting") queued_message = MessageInstanceModel( diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py index 403c2323..8fbb794a 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py @@ -1,15 +1,10 @@ -"""Test_message_service.""" -import pytest from flask import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel -from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.routes.messages_controller import message_send from spiffworkflow_backend.services.message_service import MessageService from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, @@ -20,66 +15,11 @@ from spiffworkflow_backend.services.process_instance_service import ( class TestMessageService(BaseTest): - """TestMessageService.""" - - def test_message_from_api_into_running_process( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Test sending a message to a running process via the API. - - This example workflow will send a message called 'request_approval' and then wait for a response message - of 'approval_result'. This test assures that it will fire the message with the correct correlation properties - and will respond only to a message called 'approval_result' that has the matching correlation properties, - as sent by an API Call. - """ - self.payload = { - "customer_id": "Sartography", - "po_number": 1001, - "description": "We built a new feature for messages!", - "amount": "100.00", - } - self.start_sender_process(client, with_super_admin_user, "test_from_api") - self.assure_a_message_was_sent() - self.assure_there_is_a_process_waiting_on_a_message() - - # Make an API call to the service endpoint, but use the wrong po number - with pytest.raises(ApiError): - message_send("Approval Result", {"payload": {"po_number": 5001}}) - - # Should return an error when making an API call for right po number, wrong client - with pytest.raises(ApiError): - message_send( - "Approval Result", - {"payload": {"po_number": 1001, "customer_id": "jon"}}, - ) - - # No error when calling with the correct parameters - message_send( - "Approval Result", - {"payload": {"po_number": 1001, "customer_id": "Sartography"}}, - ) - - # There is no longer a waiting message - waiting_messages = ( - MessageInstanceModel.query.filter_by(message_type="receive") - .filter_by(status="ready") - .filter_by(process_instance_id=self.process_instance.id) - .all() - ) - assert len(waiting_messages) == 0 - # The process has completed - assert self.process_instance.status == "complete" - def test_single_conversation_between_two_processes( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test messages between two different running processes using a single conversation. @@ -87,7 +27,7 @@ class TestMessageService(BaseTest): we have two process instances that are communicating with each other using one conversation about an Invoice whose details are defined in the following message payload """ - self.payload = { + payload = { "customer_id": "Sartography", "po_number": 1001, "description": "We built a new feature for messages!", @@ -104,8 +44,8 @@ class TestMessageService(BaseTest): ) # Now start the main process - self.start_sender_process(client, with_super_admin_user, "test_between_processes") - self.assure_a_message_was_sent() + process_instance = self.start_sender_process(client, payload, "test_between_processes") + self.assure_a_message_was_sent(process_instance, payload) # This is typically called in a background cron process, so we will manually call it # here in the tests @@ -113,7 +53,7 @@ class TestMessageService(BaseTest): MessageService.correlate_all_message_instances() # The sender process should still be waiting on a message to be returned to it ... - self.assure_there_is_a_process_waiting_on_a_message() + self.assure_there_is_a_process_waiting_on_a_message(process_instance) # The second time we call ths process_message_isntances (again it would typically be running on cron) # it will deliver the message that was sent from the receiver back to the original sender. @@ -125,7 +65,7 @@ class TestMessageService(BaseTest): waiting_messages = ( MessageInstanceModel.query.filter_by(message_type="receive") .filter_by(status="ready") - .filter_by(process_instance_id=self.process_instance.id) + .filter_by(process_instance_id=process_instance.id) .order_by(MessageInstanceModel.id) .all() ) @@ -136,7 +76,7 @@ class TestMessageService(BaseTest): assert len(waiting_messages) == 0 # The message sender process is complete - assert self.process_instance.status == "complete" + assert process_instance.status == "complete" # The message receiver process is also complete message_receiver_process = ( @@ -146,83 +86,15 @@ class TestMessageService(BaseTest): ) assert message_receiver_process.status == "complete" - def start_sender_process( - self, - client: FlaskClient, - with_super_admin_user: UserModel, - group_name: str = "test_group", - ) -> None: - process_group_id = group_name - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) - - process_model = load_test_spec( - "test_group/message", - process_model_source_directory="message_send_one_conversation", - bpmn_file_name="message_sender.bpmn", # Slightly misnamed, it sends and receives - ) - - self.process_instance = ProcessInstanceService.create_process_instance_from_process_model_identifier( - process_model.id, - with_super_admin_user, - ) - processor_send_receive = ProcessInstanceProcessor(self.process_instance) - processor_send_receive.do_engine_steps(save=True) - task = processor_send_receive.get_all_user_tasks()[0] - human_task = self.process_instance.active_human_tasks[0] - - ProcessInstanceService.complete_form_task( - processor_send_receive, - task, - self.payload, - with_super_admin_user, - human_task, - ) - processor_send_receive.save() - - def assure_a_message_was_sent(self) -> None: - # There should be one new send message for the given process instance. - send_messages = ( - MessageInstanceModel.query.filter_by(message_type="send") - .filter_by(process_instance_id=self.process_instance.id) - .order_by(MessageInstanceModel.id) - .all() - ) - assert len(send_messages) == 1 - send_message = send_messages[0] - assert send_message.payload == self.payload, "The send message should match up with the payload" - assert send_message.name == "Request Approval" - assert send_message.status == "ready" - - def assure_there_is_a_process_waiting_on_a_message(self) -> None: - # There should be one new send message for the given process instance. - waiting_messages = ( - MessageInstanceModel.query.filter_by(message_type="receive") - .filter_by(status="ready") - .filter_by(process_instance_id=self.process_instance.id) - .order_by(MessageInstanceModel.id) - .all() - ) - assert len(waiting_messages) == 1 - waiting_message = waiting_messages[0] - self.assure_correlation_properties_are_right(waiting_message) - - def assure_correlation_properties_are_right(self, message: MessageInstanceModel) -> None: - # Correlation Properties should match up - po_curr = next(c for c in message.correlation_rules if c.name == "po_number") - customer_curr = next(c for c in message.correlation_rules if c.name == "customer_id") - assert po_curr is not None - assert customer_curr is not None - def test_can_send_message_to_multiple_process_models( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_can_send_message_to_multiple_process_models.""" process_group_id = "test_group_multi" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) + # self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) process_model_sender = load_test_spec( "test_group/message_sender", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py index f229bdf7..c23476a0 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py @@ -1,4 +1,3 @@ -"""Test Permissions.""" from flask.app import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest @@ -22,18 +21,13 @@ from spiffworkflow_backend.services.user_service import UserService # * super-admins users maybe conventionally get the user role as well # finance-admin role allows create, update, and delete of all models under the finance group class TestPermissions(BaseTest): - """TestPermissions.""" - def test_user_can_be_given_permission_to_administer_process_group( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_user_can_be_given_permission_to_administer_process_group.""" process_group_id = "group-a" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) load_test_spec( "group-a/timers_intermediate_catch_event", bpmn_file_name="timers_intermediate_catch_event.bpmn", @@ -58,7 +52,6 @@ class TestPermissions(BaseTest): def test_group_a_admin_needs_to_stay_away_from_group_b( self, app: Flask, with_db_and_bpmn_file_cleanup: None ) -> None: - """Test_group_a_admin_needs_to_stay_away_from_group_b.""" process_group_ids = ["group-a", "group-b"] process_group_a_id = process_group_ids[0] process_group_b_id = process_group_ids[1] @@ -87,7 +80,6 @@ class TestPermissions(BaseTest): self.assert_user_has_permission(group_a_admin, "update", f"/{process_group_b_id}", expected_result=False) def test_user_can_be_granted_access_through_a_group(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None: - """Test_user_can_be_granted_access_through_a_group.""" process_group_ids = ["group-a", "group-b"] process_group_a_id = process_group_ids[0] for process_group_id in process_group_ids: @@ -125,7 +117,6 @@ class TestPermissions(BaseTest): def test_user_can_be_read_models_with_global_permission( self, app: Flask, with_db_and_bpmn_file_cleanup: None ) -> None: - """Test_user_can_be_read_models_with_global_permission.""" process_group_ids = ["group-a", "group-b"] process_group_a_id = process_group_ids[0] process_group_b_id = process_group_ids[1] @@ -156,7 +147,6 @@ class TestPermissions(BaseTest): def test_user_can_access_base_path_when_given_wildcard_permission( self, app: Flask, with_db_and_bpmn_file_cleanup: None ) -> None: - """Test_user_can_access_base_path_when_given_wildcard_permission.""" group_a_admin = self.find_or_create_user() permission_target = PermissionTargetModel(uri="/process-models/%") diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index 703d84cc..4334db64 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -70,10 +70,9 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_sets_permission_correctly_on_human_task.""" - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") finance_user = self.find_or_create_user("testuser2") assert initiator_user.principal is not None @@ -138,10 +137,9 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_sets_permission_correctly_on_human_task_when_using_dict.""" - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") finance_user_three = self.find_or_create_user("testuser3") finance_user_four = self.find_or_create_user("testuser4") @@ -234,7 +232,6 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_does_not_recreate_human_tasks_on_multiple_saves.""" initiator_user = self.find_or_create_user("initiator_user") @@ -264,9 +261,8 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") finance_user_three = self.find_or_create_user("testuser3") assert initiator_user.principal is not None @@ -319,9 +315,8 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") finance_user_three = self.find_or_create_user("testuser3") assert initiator_user.principal is not None @@ -439,15 +434,14 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") process_model = load_test_spec( process_model_id="test_group/boundary_event_reset", process_model_source_directory="boundary_event_reset", ) process_instance = self.create_process_instance_from_process_model( - process_model=process_model, user=with_super_admin_user + process_model=process_model ) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True) @@ -455,7 +449,7 @@ class TestProcessInstanceProcessor(BaseTest): human_task_one = process_instance.active_human_tasks[0] spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id)) ProcessInstanceService.complete_form_task( - processor, spiff_manual_task, {}, with_super_admin_user, human_task_one + processor, spiff_manual_task, {}, process_instance.process_initiator, human_task_one ) assert ( len(process_instance.active_human_tasks) == 1 @@ -473,7 +467,7 @@ class TestProcessInstanceProcessor(BaseTest): human_task_one = process_instance.active_human_tasks[0] assert human_task_one.task_title == "Manual Task #1" processor = ProcessInstanceProcessor(process_instance) - processor.manual_complete_task(str(human_task_one.task_id), execute=True) + processor.manual_complete_task(str(human_task_one.task_id), execute=True, user=process_instance.process_initiator) processor = ProcessInstanceProcessor(process_instance) processor.resume() processor.do_engine_steps(save=True) @@ -490,22 +484,21 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") process_model = load_test_spec( process_model_id="test_group/step_through_gateway", process_model_source_directory="step_through_gateway", ) process_instance = self.create_process_instance_from_process_model( - process_model=process_model, user=with_super_admin_user + process_model=process_model ) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True) assert len(process_instance.active_human_tasks) == 1 human_task_one = process_instance.active_human_tasks[0] processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id)) - processor.manual_complete_task(str(human_task_one.task_id), execute=True) + processor.manual_complete_task(str(human_task_one.task_id), execute=True, user=process_instance.process_initiator) processor.save() processor = ProcessInstanceProcessor(process_instance) step1_task = processor.get_task_by_bpmn_identifier("step_1", processor.bpmn_process_instance) @@ -516,7 +509,7 @@ class TestProcessInstanceProcessor(BaseTest): assert gateway_task.state == TaskState.READY gateway_task = processor.bpmn_process_instance.get_tasks(TaskState.READY)[0] - processor.manual_complete_task(str(gateway_task.id), execute=True) + processor.manual_complete_task(str(gateway_task.id), execute=True, user=process_instance.process_initiator) processor.save() processor = ProcessInstanceProcessor(process_instance) gateway_task = processor.get_task_by_bpmn_identifier("Gateway_Open", processor.bpmn_process_instance) @@ -528,9 +521,8 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") finance_user_three = self.find_or_create_user("testuser3") assert initiator_user.principal is not None @@ -757,10 +749,9 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_does_not_recreate_human_tasks_on_multiple_saves.""" - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") + self.create_process_group("test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") finance_user_three = self.find_or_create_user("testuser3") assert initiator_user.principal is not None @@ -868,7 +859,6 @@ class TestProcessInstanceProcessor(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_task_data_is_set_even_if_process_instance_errors.""" process_model = load_test_spec( @@ -877,7 +867,7 @@ class TestProcessInstanceProcessor(BaseTest): process_model_source_directory="error", ) process_instance = self.create_process_instance_from_process_model( - process_model=process_model, user=with_super_admin_user + process_model=process_model ) processor = ProcessInstanceProcessor(process_instance) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py deleted file mode 100644 index 64c7f3f7..00000000 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py +++ /dev/null @@ -1,141 +0,0 @@ -# from typing import Optional -# -# from flask.app import Flask -# from tests.spiffworkflow_backend.helpers.base_test import BaseTest -# -# from spiffworkflow_backend.models.process_instance import ProcessInstanceModel -# from spiffworkflow_backend.models.process_instance_report import ( -# ProcessInstanceReportModel, -# ) -# -# # from tests.spiffworkflow_backend.helpers.test_data import find_or_create_process_group -# # from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel -# # from spiffworkflow_backend.models.permission_target import PermissionTargetModel -# -# -# def test_generate_report_with_filter_by( -# app: Flask, -# with_db_and_bpmn_file_cleanup: None, -# setup_process_instances_for_reports: list[ProcessInstanceModel], -# ) -> None: -# """Test_user_can_be_given_permission_to_administer_process_group.""" -# process_instances = setup_process_instances_for_reports -# report_metadata = { -# "filter_by": [ -# {"field_name": "grade_level", "operator": "equals", "field_value": 2} -# ] -# } -# results = do_report_with_metadata_and_instances(report_metadata, process_instances) -# assert len(results) == 2 -# names = get_names_from_results(results) -# assert names == ["kay", "jay"] -# -# -# def test_generate_report_with_filter_by_with_variable_substitution( -# app: Flask, -# with_db_and_bpmn_file_cleanup: None, -# setup_process_instances_for_reports: list[ProcessInstanceModel], -# ) -> None: -# """Test_generate_report_with_filter_by_with_variable_substitution.""" -# process_instances = setup_process_instances_for_reports -# report_metadata = { -# "filter_by": [ -# { -# "field_name": "grade_level", -# "operator": "equals", -# "field_value": "{{grade_level}}", -# } -# ] -# } -# results = do_report_with_metadata_and_instances( -# report_metadata, process_instances, {"grade_level": 1} -# ) -# assert len(results) == 1 -# names = get_names_from_results(results) -# assert names == ["ray"] -# -# -# def test_generate_report_with_order_by_and_one_field( -# app: Flask, -# with_db_and_bpmn_file_cleanup: None, -# setup_process_instances_for_reports: list[ProcessInstanceModel], -# ) -> None: -# """Test_generate_report_with_order_by_and_one_field.""" -# process_instances = setup_process_instances_for_reports -# report_metadata = {"order_by": ["test_score"]} -# results = do_report_with_metadata_and_instances(report_metadata, process_instances) -# assert len(results) == 3 -# names = get_names_from_results(results) -# assert names == ["jay", "ray", "kay"] -# -# -# def test_generate_report_with_order_by_and_two_fields( -# app: Flask, -# with_db_and_bpmn_file_cleanup: None, -# setup_process_instances_for_reports: list[ProcessInstanceModel], -# ) -> None: -# """Test_generate_report_with_order_by_and_two_fields.""" -# process_instances = setup_process_instances_for_reports -# report_metadata = {"order_by": ["grade_level", "test_score"]} -# results = do_report_with_metadata_and_instances(report_metadata, process_instances) -# assert len(results) == 3 -# names = get_names_from_results(results) -# assert names == ["ray", "jay", "kay"] -# -# -# def test_generate_report_with_order_by_desc( -# app: Flask, -# with_db_and_bpmn_file_cleanup: None, -# setup_process_instances_for_reports: list[ProcessInstanceModel], -# ) -> None: -# """Test_generate_report_with_order_by_desc.""" -# process_instances = setup_process_instances_for_reports -# report_metadata = {"order_by": ["grade_level", "-test_score"]} -# results = do_report_with_metadata_and_instances(report_metadata, process_instances) -# assert len(results) == 3 -# names = get_names_from_results(results) -# assert names == ["ray", "kay", "jay"] -# -# -# def test_generate_report_with_columns( -# app: Flask, -# with_db_and_bpmn_file_cleanup: None, -# setup_process_instances_for_reports: list[ProcessInstanceModel], -# ) -> None: -# """Test_generate_report_with_columns.""" -# process_instances = setup_process_instances_for_reports -# report_metadata = { -# "columns": [ -# {"Header": "Name", "accessor": "name"}, -# {"Header": "Status", "accessor": "status"}, -# ], -# "order_by": ["test_score"], -# "filter_by": [ -# {"field_name": "grade_level", "operator": "equals", "field_value": 1} -# ], -# } -# results = do_report_with_metadata_and_instances(report_metadata, process_instances) -# assert len(results) == 1 -# assert results == [{"name": "ray", "status": "complete"}] -# -# -# def do_report_with_metadata_and_instances( -# report_metadata: dict, -# process_instances: list[ProcessInstanceModel], -# substitution_variables: Optional[dict] = None, -# ) -> list[dict]: -# """Do_report_with_metadata_and_instances.""" -# process_instance_report = ProcessInstanceReportModel.create_report( -# identifier="sure", -# report_metadata=report_metadata, -# user=BaseTest.find_or_create_user(), -# ) -# -# return process_instance_report.generate_report( -# process_instances, substitution_variables -# )["results"] -# -# -# def get_names_from_results(results: list[dict]) -> list[str]: -# """Get_names_from_results.""" -# return [result["name"] for result in results] diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py index 54e99673..2257ea1f 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py @@ -40,7 +40,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: model = ProcessInstanceService.file_data_model_for_value( "uploaded_file", @@ -53,7 +52,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: model = ProcessInstanceService.file_data_model_for_value( "not_a_file", @@ -66,7 +64,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "uploaded_file": self.SAMPLE_FILE_DATA, @@ -80,7 +77,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "uploaded_files": [self.SAMPLE_FILE_DATA, self.SAMPLE_FILE_DATA], @@ -95,7 +91,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "not_a_file": "just a value", @@ -120,7 +115,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "not_a_file": "just a value", @@ -135,7 +129,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "uploaded_file": self.SAMPLE_FILE_DATA, @@ -156,7 +149,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "not_a_file": "just a value", @@ -171,7 +163,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "not_a_file": "just a value", @@ -198,7 +189,6 @@ class TestProcessInstanceService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: data = { "File": [ diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py index d8b90a8a..863b58b4 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py @@ -18,9 +18,7 @@ class TestRestrictedScriptEngine(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") process_model = load_test_spec( "test_group/dangerous", bpmn_file_name="read_etc_passwd.bpmn", @@ -40,9 +38,7 @@ class TestRestrictedScriptEngine(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") process_model = load_test_spec( "test_group/dangerous", bpmn_file_name="read_env.bpmn", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py index f5eef2e8..0bd909a4 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py @@ -1,4 +1,3 @@ -"""Test Permissions.""" from flask.app import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest @@ -13,20 +12,15 @@ from spiffworkflow_backend.services.script_unit_test_runner import ScriptUnitTes class TestScriptUnitTestRunner(BaseTest): - """TestScriptUnitTestRunner.""" - def test_takes_data_and_returns_expected_result( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_takes_data_and_returns_expected_result.""" app.config["THREAD_LOCAL_DATA"].process_instance_id = None process_group_id = "test_logging_spiff_logger" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) process_model_id = "simple_script" process_model_identifier = f"{process_group_id}/{process_model_id}" load_test_spec( @@ -56,14 +50,10 @@ class TestScriptUnitTestRunner(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_fails_when_expected_output_does_not_match_actual_output.""" app.config["THREAD_LOCAL_DATA"].process_instance_id = None process_group_id = "test_logging_spiff_logger" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) - process_model_id = "simple_script" process_model_identifier = f"{process_group_id}/{process_model_id}" load_test_spec( @@ -93,14 +83,10 @@ class TestScriptUnitTestRunner(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_script_with_unit_tests_when_hey_is_passed_in.""" app.config["THREAD_LOCAL_DATA"].process_instance_id = None process_group_id = "script_with_unit_tests" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) - process_model_id = "script_with_unit_tests" process_model_identifier = f"{process_group_id}/{process_model_id}" load_test_spec( @@ -126,14 +112,10 @@ class TestScriptUnitTestRunner(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_script_with_unit_tests_when_hey_is_not_passed_in.""" app.config["THREAD_LOCAL_DATA"].process_instance_id = None process_group_id = "script_with_unit_tests" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) - process_model_id = "script_with_unit_tests" process_model_identifier = f"{process_group_id}/{process_model_id}" diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py index 65002dcc..fa560e0e 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py @@ -22,8 +22,10 @@ from spiffworkflow_backend.services.spec_file_service import SpecFileService class TestSpecFileService(BaseTest): """TestSpecFileService.""" - process_group_id = "test_process_group_id" - process_model_id = "call_activity_nested" + process_group_id = "" + process_model_id = "test_process_group_id/call_activity_nested" + # process_group_id = "test_process_group_id" + # process_model_id = "call_activity_nested" bpmn_file_name = "call_activity_nested.bpmn" call_activity_nested_relative_file_path = os.path.join(process_group_id, process_model_id, bpmn_file_name) @@ -33,16 +35,11 @@ class TestSpecFileService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_can_store_process_ids_for_lookup.""" - self.create_group_and_model_with_bpmn( - client=client, - user=with_super_admin_user, - process_group_id=self.process_group_id, + load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - bpmn_file_location="call_activity_nested", + process_model_source_directory='call_activity_nested', ) bpmn_process_id_lookups = SpecReferenceCache.query.all() assert len(bpmn_process_id_lookups) == 1 @@ -54,17 +51,13 @@ class TestSpecFileService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_fails_to_save_duplicate_process_id.""" bpmn_process_identifier = "Level1" - self.create_group_and_model_with_bpmn( - client=client, - user=with_super_admin_user, - process_group_id=self.process_group_id, + load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - bpmn_file_location=self.process_model_id, + process_model_source_directory='call_activity_nested', ) bpmn_process_id_lookups = SpecReferenceCache.query.all() assert len(bpmn_process_id_lookups) == 1 @@ -87,7 +80,6 @@ class TestSpecFileService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_updates_relative_file_path_when_appropriate.""" bpmn_process_identifier = "Level1" @@ -99,13 +91,10 @@ class TestSpecFileService(BaseTest): db.session.add(process_id_lookup) db.session.commit() - self.create_group_and_model_with_bpmn( - client=client, - user=with_super_admin_user, - process_group_id=self.process_group_id, + load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - bpmn_file_location=self.process_model_id, + process_model_source_directory='call_activity_nested', ) bpmn_process_id_lookups = SpecReferenceCache.query.all() @@ -139,7 +128,6 @@ class TestSpecFileService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """When a BPMN processes identifier is changed in a file, the old id is removed from the cache.""" old_identifier = "ye_old_identifier" @@ -147,19 +135,16 @@ class TestSpecFileService(BaseTest): identifier=old_identifier, relative_path=self.call_activity_nested_relative_file_path, file_name=self.bpmn_file_name, - process_model_id=f"{self.process_group_id}/{self.process_model_id}", + process_model_id=self.process_model_id, type="process", ) db.session.add(process_id_lookup) db.session.commit() - self.create_group_and_model_with_bpmn( - client=client, - user=with_super_admin_user, - process_group_id=self.process_group_id, + load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - bpmn_file_location=self.process_model_id, + process_model_source_directory='call_activity_nested', ) bpmn_process_id_lookups = SpecReferenceCache.query.all() @@ -173,7 +158,6 @@ class TestSpecFileService(BaseTest): app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_load_reference_information. @@ -186,32 +170,22 @@ class TestSpecFileService(BaseTest): a DMN file can (theoretically) contain many decisions. So this is an array. """ - process_group_id = "test_group" - process_model_id = "call_activity_nested" - process_model_identifier = self.create_group_and_model_with_bpmn( - client=client, - user=with_super_admin_user, - process_group_id=process_group_id, + process_model_id = "test_group/call_activity_nested" + process_model = load_test_spec( process_model_id=process_model_id, - # bpmn_file_name=bpmn_file_name, - bpmn_file_location=process_model_id, + process_model_source_directory='call_activity_nested', ) - # load_test_spec( - # , - # process_model_source_directory="call_activity_nested", - # ) - process_model_info = ProcessModelService.get_process_model(process_model_identifier) - files = SpecFileService.get_files(process_model_info) + files = SpecFileService.get_files(process_model) file = next(filter(lambda f: f.name == "call_activity_level_3.bpmn", files)) - ca_3 = SpecFileService.get_references_for_file(file, process_model_info) + ca_3 = SpecFileService.get_references_for_file(file, process_model) assert len(ca_3) == 1 assert ca_3[0].display_name == "Level 3" assert ca_3[0].identifier == "Level3" assert ca_3[0].type == "process" file = next(filter(lambda f: f.name == "level2c.dmn", files)) - dmn1 = SpecFileService.get_references_for_file(file, process_model_info) + dmn1 = SpecFileService.get_references_for_file(file, process_model) assert len(dmn1) == 1 assert dmn1[0].display_name == "Decision 1" assert dmn1[0].identifier == "Decision_0vrtcmk" diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py index fa25917e..d13f71c1 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py @@ -1,5 +1,6 @@ """Test_various_bpmn_constructs.""" from flask.app import Flask +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest @@ -11,25 +12,16 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer class TestVariousBpmnConstructs(BaseTest): - """TestVariousBpmnConstructs.""" - def test_running_process_with_timer_intermediate_catch_event( self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_running_process_with_timer_intermediate_catch_event.""" - process_model_identifier = self.create_group_and_model_with_bpmn( - client, - with_super_admin_user, - "test_group", - "timer_intermediate_catch_event", + process_model = load_test_spec( + process_model_id="test_group/timer_intermediate_catch_event", + process_model_source_directory='timer_intermediate_catch_event', ) - - process_model = ProcessModelService.get_process_model(process_model_id=process_model_identifier) - process_instance = self.create_process_instance_from_process_model(process_model) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True) diff --git a/spiffworkflow-frontend/src/components/ActiveUsers.tsx b/spiffworkflow-frontend/src/components/ActiveUsers.tsx index 7bd282b0..aa972275 100644 --- a/spiffworkflow-frontend/src/components/ActiveUsers.tsx +++ b/spiffworkflow-frontend/src/components/ActiveUsers.tsx @@ -18,6 +18,7 @@ export default function ActiveUsers() { HttpService.makeCallToBackend({ path: `/active-users/updates/${lastVisitedIdentifier}`, successCallback: setActiveUsers, + httpMethod: 'POST', }); }; @@ -25,6 +26,7 @@ export default function ActiveUsers() { HttpService.makeCallToBackend({ path: `/active-users/unregister/${lastVisitedIdentifier}`, successCallback: setActiveUsers, + httpMethod: 'POST', }); }; updateActiveUsers(); From 3124c5451a7e3daee18355c904ba88c8b04cf626 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 22 May 2023 14:09:17 -0400 Subject: [PATCH 36/60] remove api usages from script tests and pyl w/ burnettk --- spiffworkflow-backend/conftest.py | 12 +- .../scripts/save_process_instance_metadata.py | 43 ---- .../services/process_instance_processor.py | 1 - .../save_process_instance_metadata.bpmn | 52 ----- .../helpers/base_test.py | 12 +- .../scripts/test_get_all_permissions.py | 10 - .../scripts/test_get_current_user.py | 5 - .../scripts/test_get_group_members.py | 9 - .../test_get_last_user_completing_task.py | 7 - .../scripts/test_get_localtime.py | 8 - .../test_get_process_initiator_user.py | 6 - .../scripts/test_refresh_permissions.py | 2 - .../test_save_process_instance_metadata.py | 42 ---- .../unit/test_authorization_service.py | 188 +++++++++--------- .../unit/test_dot_notation.py | 7 +- .../unit/test_error_handling_service.py | 12 +- .../unit/test_message_instance.py | 6 +- .../unit/test_message_service.py | 1 - .../unit/test_permissions.py | 1 - .../unit/test_process_instance_processor.py | 21 +- .../unit/test_process_instance_service.py | 1 - .../unit/test_restricted_script_engine.py | 6 - .../unit/test_script_unit_test_runner.py | 1 - .../unit/test_spec_file_service.py | 11 +- .../unit/test_various_bpmn_constructs.py | 6 +- 25 files changed, 128 insertions(+), 342 deletions(-) delete mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py delete mode 100644 spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn delete mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index 7006c17a..e7102b00 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -4,19 +4,11 @@ import shutil import pytest from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.db import db -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.services.process_instance_processor import ( - ProcessInstanceProcessor, -) -from spiffworkflow_backend.services.process_instance_service import ( - ProcessInstanceService, -) from spiffworkflow_backend.services.process_model_service import ProcessModelService @@ -62,5 +54,5 @@ def with_db_and_bpmn_file_cleanup() -> None: @pytest.fixture() def with_super_admin_user() -> UserModel: - raise Exception("HEY") - # return BaseTest.create_user_with_permission("super_admin") + """With_super_admin_user.""" + return BaseTest.create_user_with_permission("super_admin") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py deleted file mode 100644 index 8fe8d3e8..00000000 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/save_process_instance_metadata.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Save process instance metadata.""" -from typing import Any - -from spiffworkflow_backend.models.db import db -from spiffworkflow_backend.models.process_instance_metadata import ( - ProcessInstanceMetadataModel, -) -from spiffworkflow_backend.models.script_attributes_context import ( - ScriptAttributesContext, -) -from spiffworkflow_backend.scripts.script import Script - - -class SaveProcessInstanceMetadata(Script): - """SaveProcessInstanceMetadata.""" - - def get_description(self) -> str: - """Get_description.""" - return """Save a given dict as process instance metadata (useful for creating reports).""" - - def run( - self, - script_attributes_context: ScriptAttributesContext, - *args: Any, - **kwargs: Any, - ) -> Any: - """Run.""" - metadata_dict = args[0] - if script_attributes_context.process_instance_id is None: - raise self.get_proces_instance_id_is_missing_error("save_process_instance_metadata") - for key, value in metadata_dict.items(): - pim = ProcessInstanceMetadataModel.query.filter_by( - process_instance_id=script_attributes_context.process_instance_id, - key=key, - ).first() - if pim is None: - pim = ProcessInstanceMetadataModel( - process_instance_id=script_attributes_context.process_instance_id, - key=key, - ) - pim.value = value - db.session.add(pim) - db.session.commit() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 60d160c6..31a85ff0 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -25,7 +25,6 @@ from uuid import UUID import dateparser import pytz from flask import current_app -from flask import g from lxml import etree # type: ignore from lxml.etree import XMLSyntaxError # type: ignore from RestrictedPython import safe_globals # type: ignore diff --git a/spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn b/spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn deleted file mode 100644 index 2c72b08d..00000000 --- a/spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn +++ /dev/null @@ -1,52 +0,0 @@ - - - - - Flow_1j4jzft - - - - Flow_01xr2ac - - - Flow_1j4jzft - Flow_10xyk22 - save_process_instance_metadata({"key1": "value1"}) - - - - Flow_10xyk22 - Flow_01xr2ac - save_process_instance_metadata({"key2": "value2", "key3": "value3"}) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 499567cf..46970453 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -9,14 +9,12 @@ from typing import Optional from flask import current_app from flask.testing import FlaskClient -from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor -from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from werkzeug.test import TestResponse # type: ignore -from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.models.db import db +from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.permission_assignment import Permission from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.process_group import ProcessGroup @@ -29,9 +27,11 @@ from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor from spiffworkflow_backend.services.process_instance_queue_service import ( ProcessInstanceQueueService, ) +from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.user_service import UserService @@ -319,7 +319,7 @@ class BaseTest: ) -> UserModel: # user = BaseTest.find_or_create_user(username=username) # return cls.add_permissions_to_user(user, target_uri=target_uri, permission_names=permission_names) - user = BaseTest.find_or_create_user(username='testadmin1') + user = BaseTest.find_or_create_user(username="testadmin1") AuthorizationService.import_permissions_from_yaml_file(user) return user @@ -418,9 +418,7 @@ class BaseTest: bpmn_file_name="message_sender.bpmn", # Slightly misnamed, it sends and receives ) - process_instance = self.create_process_instance_from_process_model( - process_model - ) + process_instance = self.create_process_instance_from_process_model(process_model) processor_send_receive = ProcessInstanceProcessor(process_instance) processor_send_receive.do_engine_steps(save=True) task = processor_send_receive.get_all_user_tasks()[0] diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py index d12d5072..581ec405 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py @@ -1,31 +1,21 @@ -"""Test_get_localtime.""" from operator import itemgetter from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.script_attributes_context import ( ScriptAttributesContext, ) -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.scripts.get_all_permissions import GetAllPermissions from spiffworkflow_backend.services.authorization_service import AuthorizationService class TestGetAllPermissions(BaseTest): - """TestGetAllPermissions.""" - def test_can_get_all_permissions( self, app: Flask, - client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_can_get_all_permissions.""" - self.find_or_create_user("test_user") - # now that we have everything, try to clear it out... script_attributes_context = ScriptAttributesContext( task=None, diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_current_user.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_current_user.py index c2c0f674..291cb43b 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_current_user.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_current_user.py @@ -3,14 +3,12 @@ import json from flask import g from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.script_attributes_context import ( ScriptAttributesContext, ) -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.scripts.get_current_user import GetCurrentUser @@ -18,11 +16,8 @@ class TestGetCurrentUser(BaseTest): def test_get_current_user( self, app: Flask, - client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_can_get_members_of_a_group.""" testuser1 = self.find_or_create_user("testuser1") testuser1.tenant_specific_field_1 = "456" db.session.add(testuser1) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py index 685788c3..cd1bf1d8 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_group_members.py @@ -1,12 +1,9 @@ -"""Test_get_localtime.""" from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.group import GroupModel -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) @@ -14,16 +11,11 @@ from spiffworkflow_backend.services.user_service import UserService class TestGetGroupMembers(BaseTest): - """TestGetGroupMembers.""" - def test_can_get_members_of_a_group( self, app: Flask, - client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_can_get_members_of_a_group.""" initiator_user = self.find_or_create_user("initiator_user") testuser1 = self.find_or_create_user("testuser1") testuser2 = self.find_or_create_user("testuser2") @@ -38,7 +30,6 @@ class TestGetGroupMembers(BaseTest): UserService.add_user_to_group(testuser2, group_a) UserService.add_user_to_group(testuser3, group_b) - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") process_model = load_test_spec( process_model_id="test_group/get_group_members", bpmn_file_name="get_group_members.bpmn", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_last_user_completing_task.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_last_user_completing_task.py index fcd8b641..4bc8741f 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_last_user_completing_task.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_last_user_completing_task.py @@ -1,10 +1,7 @@ -"""Test_get_localtime.""" from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, @@ -18,12 +15,8 @@ class TestGetLastUserCompletingTask(BaseTest): def test_get_last_user_completing_task_script_works( self, app: Flask, - client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: - """Test_sets_permission_correctly_on_human_task.""" - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") assert initiator_user.principal is not None AuthorizationService.import_permissions_from_yaml_file() diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_localtime.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_localtime.py index 9595c948..954ef7fa 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_localtime.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_localtime.py @@ -1,9 +1,7 @@ -"""Test_get_localtime.""" import datetime import pytz from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec @@ -20,10 +18,7 @@ from spiffworkflow_backend.services.process_instance_service import ( class TestGetLocaltime(BaseTest): - """TestProcessAPi.""" - def test_get_localtime_script_directly(self) -> None: - """Test_get_localtime_script_directly.""" current_time = datetime.datetime.now() timezone = "US/Pacific" process_model_identifier = "test_process_model" @@ -44,17 +39,14 @@ class TestGetLocaltime(BaseTest): def test_get_localtime_script_through_bpmn( self, app: Flask, - client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - """Test_process_instance_run.""" initiator_user = self.find_or_create_user("initiator_user") self.add_permissions_to_user( initiator_user, target_uri="/v1.0/process-groups", permission_names=["read", "create"], ) - self.create_process_group_with_api(client=client, user=initiator_user, process_group_id="test_group") process_model = load_test_spec( process_model_id="test_group/get_localtime", bpmn_file_name="get_localtime.bpmn", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_process_initiator_user.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_process_initiator_user.py index 60a93f9a..566d510a 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_process_initiator_user.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_process_initiator_user.py @@ -1,10 +1,8 @@ """Test_get_localtime.""" from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, @@ -18,19 +16,15 @@ class TestGetProcessInitiatorUser(BaseTest): def test_get_process_initiator_user( self, app: Flask, - client: FlaskClient, with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, ) -> None: """Test_sets_permission_correctly_on_human_task.""" - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") assert initiator_user.principal is not None AuthorizationService.import_permissions_from_yaml_file() process_model = load_test_spec( process_model_id="misc/category_number_one/simple_form", - # bpmn_file_name="simp.bpmn", process_model_source_directory="simple_form", ) process_instance = self.create_process_instance_from_process_model( diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_refresh_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_refresh_permissions.py index 225e870f..08a5969d 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_refresh_permissions.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_refresh_permissions.py @@ -1,7 +1,6 @@ """Test_get_localtime.""" import pytest from flask.app import Flask -from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec @@ -15,7 +14,6 @@ class TestRefreshPermissions(BaseTest): def test_refresh_permissions_requires_elevated_permission( self, app: Flask, - client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: basic_user = self.find_or_create_user("basic_user") diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py deleted file mode 100644 index bf64b21d..00000000 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_save_process_instance_metadata.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Test_get_localtime.""" -from flask.app import Flask -from flask.testing import FlaskClient -from tests.spiffworkflow_backend.helpers.base_test import BaseTest -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec - -from spiffworkflow_backend.models.process_instance_metadata import ( - ProcessInstanceMetadataModel, -) -from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.services.process_instance_processor import ( - ProcessInstanceProcessor, -) - - -class TestSaveProcessInstanceMetadata(BaseTest): - """TestSaveProcessInstanceMetadata.""" - - def test_can_save_process_instance_metadata( - self, - app: Flask, - client: FlaskClient, - with_db_and_bpmn_file_cleanup: None, - with_super_admin_user: UserModel, - ) -> None: - """Test_can_save_process_instance_metadata.""" - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") - process_model = load_test_spec( - process_model_id="save_process_instance_metadata/save_process_instance_metadata", - bpmn_file_name="save_process_instance_metadata.bpmn", - process_model_source_directory="save_process_instance_metadata", - ) - process_instance = self.create_process_instance_from_process_model( - process_model=process_model, user=with_super_admin_user - ) - processor = ProcessInstanceProcessor(process_instance) - processor.do_engine_steps(save=True) - - process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by( - process_instance_id=process_instance.id - ).all() - assert len(process_instance_metadata) == 3 diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 514b2cea..3db9e725 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -1,12 +1,11 @@ """Test_message_service.""" import pytest -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from spiffworkflow_backend.models.group import GroupModel -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import GroupPermissionsDict from spiffworkflow_backend.services.authorization_service import InvalidPermissionError @@ -17,7 +16,6 @@ from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_service import ( ProcessInstanceService, ) -from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.user_service import UserService @@ -137,7 +135,7 @@ class TestAuthorizationService(BaseTest): "delete", ), ("/process-instances/some-process-group:some-process-model:*", "read"), - ('/process-model-natural-language/some-process-group:some-process-model:*', "create"), + ("/process-model-natural-language/some-process-group:some-process-model:*", "create"), ("/process-model-publish/some-process-group:some-process-model:*", "create"), ("/process-models/some-process-group:some-process-model:*", "create"), ("/process-models/some-process-group:some-process-model:*", "delete"), @@ -160,26 +158,28 @@ class TestAuthorizationService(BaseTest): with_db_and_bpmn_file_cleanup: None, ) -> None: """Test_explode_permissions_start_on_process_group.""" - expected_permissions = sorted([ - ("/event-error-details/some-process-group:some-process-model:*", "read"), - ( - "/logs/some-process-group:some-process-model:*", - "read", - ), - ( - "/logs/typeahead-filter-values/some-process-group:some-process-model:*", - "read", - ), - ( - "/process-data-file-download/some-process-group:some-process-model:*", - "read", - ), - ( - "/process-instances/for-me/some-process-group:some-process-model:*", - "read", - ), - ("/process-instances/some-process-group:some-process-model:*", "create"), - ]) + expected_permissions = sorted( + [ + ("/event-error-details/some-process-group:some-process-model:*", "read"), + ( + "/logs/some-process-group:some-process-model:*", + "read", + ), + ( + "/logs/typeahead-filter-values/some-process-group:some-process-model:*", + "read", + ), + ( + "/process-data-file-download/some-process-group:some-process-model:*", + "read", + ), + ( + "/process-instances/for-me/some-process-group:some-process-model:*", + "read", + ), + ("/process-instances/some-process-group:some-process-model:*", "create"), + ] + ) permissions_to_assign = AuthorizationService.explode_permissions( "start", "PG:/some-process-group/some-process-model" ) @@ -219,7 +219,7 @@ class TestAuthorizationService(BaseTest): "delete", ), ("/process-instances/some-process-group:some-process-model/*", "read"), - ('/process-model-natural-language/some-process-group:some-process-model/*', "create"), + ("/process-model-natural-language/some-process-group:some-process-model/*", "create"), ("/process-model-publish/some-process-group:some-process-model/*", "create"), ("/process-models/some-process-group:some-process-model/*", "create"), ("/process-models/some-process-group:some-process-model/*", "delete"), @@ -242,26 +242,28 @@ class TestAuthorizationService(BaseTest): with_db_and_bpmn_file_cleanup: None, ) -> None: """Test_explode_permissions_start_on_process_model.""" - expected_permissions = sorted([ - ( - "/event-error-details/some-process-group:some-process-model/*", - "read", - ), - ( - "/logs/some-process-group:some-process-model/*", - "read", - ), - ("/logs/typeahead-filter-values/some-process-group:some-process-model/*", "read"), - ( - "/process-data-file-download/some-process-group:some-process-model/*", - "read", - ), - ( - "/process-instances/for-me/some-process-group:some-process-model/*", - "read", - ), - ("/process-instances/some-process-group:some-process-model/*", "create"), - ]) + expected_permissions = sorted( + [ + ( + "/event-error-details/some-process-group:some-process-model/*", + "read", + ), + ( + "/logs/some-process-group:some-process-model/*", + "read", + ), + ("/logs/typeahead-filter-values/some-process-group:some-process-model/*", "read"), + ( + "/process-data-file-download/some-process-group:some-process-model/*", + "read", + ), + ( + "/process-instances/for-me/some-process-group:some-process-model/*", + "read", + ), + ("/process-instances/some-process-group:some-process-model/*", "create"), + ] + ) permissions_to_assign = AuthorizationService.explode_permissions( "start", "PM:/some-process-group/some-process-model" ) @@ -274,30 +276,32 @@ class TestAuthorizationService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - expected_permissions = sorted([ - ("/active-users/*", "create"), - ("/connector-proxy/typeahead/*", "read"), - ("/debug/version-info", "read"), - ("/process-groups", "read"), - ("/process-instances/find-by-id/*", "read"), - ("/process-instances/for-me", "create"), - ("/process-instances/report-metadata", "read"), - ("/process-instances/reports/*", "create"), - ("/process-instances/reports/*", "delete"), - ("/process-instances/reports/*", "read"), - ("/process-instances/reports/*", "update"), - ("/process-models", "read"), - ("/processes", "read"), - ("/processes/callers", "read"), - ("/service-tasks", "read"), - ("/tasks/*", "create"), - ("/tasks/*", "delete"), - ("/tasks/*", "read"), - ("/tasks/*", "update"), - ("/user-groups/for-current-user", "read"), - ("/users/exists/by-username", "create"), - ("/users/search", "read"), - ]) + expected_permissions = sorted( + [ + ("/active-users/*", "create"), + ("/connector-proxy/typeahead/*", "read"), + ("/debug/version-info", "read"), + ("/process-groups", "read"), + ("/process-instances/find-by-id/*", "read"), + ("/process-instances/for-me", "create"), + ("/process-instances/report-metadata", "read"), + ("/process-instances/reports/*", "create"), + ("/process-instances/reports/*", "delete"), + ("/process-instances/reports/*", "read"), + ("/process-instances/reports/*", "update"), + ("/process-models", "read"), + ("/processes", "read"), + ("/processes/callers", "read"), + ("/service-tasks", "read"), + ("/tasks/*", "create"), + ("/tasks/*", "delete"), + ("/tasks/*", "read"), + ("/tasks/*", "update"), + ("/user-groups/for-current-user", "read"), + ("/users/exists/by-username", "create"), + ("/users/search", "read"), + ] + ) permissions_to_assign = AuthorizationService.explode_permissions("all", "BASIC") permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) assert permissions_to_assign_tuples == expected_permissions @@ -308,28 +312,30 @@ class TestAuthorizationService(BaseTest): client: FlaskClient, with_db_and_bpmn_file_cleanup: None, ) -> None: - expected_permissions = sorted([ - ("/authentications", "read"), - ("/can-run-privileged-script/*", "create"), - ("/debug/*", "create"), - ("/messages", "read"), - ("/messages/*", "create"), - ("/process-instance-reset/*", "create"), - ("/process-instance-resume/*", "create"), - ("/process-instance-suspend/*", "create"), - ("/process-instance-terminate/*", "create"), - ("/process-instances/*", "create"), - ("/process-instances/*", "delete"), - ("/process-instances/*", "read"), - ("/process-instances/*", "update"), - ("/secrets/*", "create"), - ("/secrets/*", "delete"), - ("/secrets/*", "read"), - ("/secrets/*", "update"), - ("/send-event/*", "create"), - ("/task-complete/*", "create"), - ("/task-data/*", "update"), - ]) + expected_permissions = sorted( + [ + ("/authentications", "read"), + ("/can-run-privileged-script/*", "create"), + ("/debug/*", "create"), + ("/messages", "read"), + ("/messages/*", "create"), + ("/process-instance-reset/*", "create"), + ("/process-instance-resume/*", "create"), + ("/process-instance-suspend/*", "create"), + ("/process-instance-terminate/*", "create"), + ("/process-instances/*", "create"), + ("/process-instances/*", "delete"), + ("/process-instances/*", "read"), + ("/process-instances/*", "update"), + ("/secrets/*", "create"), + ("/secrets/*", "delete"), + ("/secrets/*", "read"), + ("/secrets/*", "update"), + ("/send-event/*", "create"), + ("/task-complete/*", "create"), + ("/task-data/*", "update"), + ] + ) permissions_to_assign = AuthorizationService.explode_permissions("all", "ELEVATED") permissions_to_assign_tuples = sorted([(p.target_uri, p.permission) for p in permissions_to_assign]) assert permissions_to_assign_tuples == expected_permissions diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py index 2a4ad999..a8ca1e6e 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_dot_notation.py @@ -1,10 +1,9 @@ """Test_various_bpmn_constructs.""" from flask.app import Flask -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) @@ -42,7 +41,9 @@ class TestDotNotation(BaseTest): "invoice.invoiceAmount": "1000.00", "invoice.dueDate": "09/30/2022", } - ProcessInstanceService.complete_form_task(processor, user_task, form_data, process_instance.process_initiator, human_task) + ProcessInstanceService.complete_form_task( + processor, user_task, form_data, process_instance.process_initiator, human_task + ) expected = { "contibutorName": "Elizabeth", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py index 6c3d893b..8ea4a5b7 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_error_handling_service.py @@ -9,14 +9,10 @@ from spiffworkflow_backend.models.message_instance import MessageInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_model import ProcessModelInfo -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) -from spiffworkflow_backend.services.process_instance_service import ( - ProcessInstanceService, -) from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.workflow_execution_service import WorkflowExecutionServiceError @@ -27,12 +23,8 @@ class TestErrorHandlingService(BaseTest): Like it can fire off BPMN messages in case a BPMN Task is waiting for that message. """ - def run_process_model_and_handle_error( - self, process_model: ProcessModelInfo - ) -> ProcessInstanceModel: - process_instance = self.create_process_instance_from_process_model( - process_model - ) + def run_process_model_and_handle_error(self, process_model: ProcessModelInfo) -> ProcessInstanceModel: + process_instance = self.create_process_instance_from_process_model(process_model) pip = ProcessInstanceProcessor(process_instance) with pytest.raises(WorkflowExecutionServiceError) as e: pip.do_engine_steps(save=True) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py index 9f651d76..ab7ac00b 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_instance.py @@ -1,15 +1,13 @@ """Test_message_instance.""" import pytest -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask import Flask from flask.testing import FlaskClient -from spiffworkflow_backend.models.process_model import ProcessModelInfo from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.message_instance import MessageInstanceModel -from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.models.process_model import ProcessModelInfo class TestMessageInstance(BaseTest): diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py index 8fbb794a..f083d3a9 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_message_service.py @@ -93,7 +93,6 @@ class TestMessageService(BaseTest): with_db_and_bpmn_file_cleanup: None, ) -> None: """Test_can_send_message_to_multiple_process_models.""" - process_group_id = "test_group_multi" # self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_id) process_model_sender = load_test_spec( diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py index c23476a0..9e7ef1cb 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py @@ -8,7 +8,6 @@ from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel from spiffworkflow_backend.models.permission_target import PermissionTargetModel from spiffworkflow_backend.models.principal import PrincipalModel -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.user_service import UserService diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index 4334db64..18bdd759 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -18,7 +18,6 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.models.task_definition import TaskDefinitionModel -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import ( UserDoesNotHaveAccessToTaskError, @@ -440,9 +439,7 @@ class TestProcessInstanceProcessor(BaseTest): process_model_id="test_group/boundary_event_reset", process_model_source_directory="boundary_event_reset", ) - process_instance = self.create_process_instance_from_process_model( - process_model=process_model - ) + process_instance = self.create_process_instance_from_process_model(process_model=process_model) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True) assert len(process_instance.active_human_tasks) == 1 @@ -467,7 +464,9 @@ class TestProcessInstanceProcessor(BaseTest): human_task_one = process_instance.active_human_tasks[0] assert human_task_one.task_title == "Manual Task #1" processor = ProcessInstanceProcessor(process_instance) - processor.manual_complete_task(str(human_task_one.task_id), execute=True, user=process_instance.process_initiator) + processor.manual_complete_task( + str(human_task_one.task_id), execute=True, user=process_instance.process_initiator + ) processor = ProcessInstanceProcessor(process_instance) processor.resume() processor.do_engine_steps(save=True) @@ -490,15 +489,15 @@ class TestProcessInstanceProcessor(BaseTest): process_model_id="test_group/step_through_gateway", process_model_source_directory="step_through_gateway", ) - process_instance = self.create_process_instance_from_process_model( - process_model=process_model - ) + process_instance = self.create_process_instance_from_process_model(process_model=process_model) processor = ProcessInstanceProcessor(process_instance) processor.do_engine_steps(save=True) assert len(process_instance.active_human_tasks) == 1 human_task_one = process_instance.active_human_tasks[0] processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id)) - processor.manual_complete_task(str(human_task_one.task_id), execute=True, user=process_instance.process_initiator) + processor.manual_complete_task( + str(human_task_one.task_id), execute=True, user=process_instance.process_initiator + ) processor.save() processor = ProcessInstanceProcessor(process_instance) step1_task = processor.get_task_by_bpmn_identifier("step_1", processor.bpmn_process_instance) @@ -866,9 +865,7 @@ class TestProcessInstanceProcessor(BaseTest): bpmn_file_name="script_error_with_task_data.bpmn", process_model_source_directory="error", ) - process_instance = self.create_process_instance_from_process_model( - process_model=process_model - ) + process_instance = self.create_process_instance_from_process_model(process_model=process_model) processor = ProcessInstanceProcessor(process_instance) with pytest.raises(WorkflowExecutionServiceError): diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py index 2257ea1f..274245e5 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_service.py @@ -9,7 +9,6 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.process_instance_file_data import ( ProcessInstanceFileDataModel, ) -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_service import ( ProcessInstanceService, ) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py index 863b58b4..27788ce8 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py @@ -1,11 +1,9 @@ -"""Test_various_bpmn_constructs.""" import pytest from flask.app import Flask from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) @@ -24,8 +22,6 @@ class TestRestrictedScriptEngine(BaseTest): bpmn_file_name="read_etc_passwd.bpmn", process_model_source_directory="dangerous-scripts", ) - self.find_or_create_user() - process_instance = self.create_process_instance_from_process_model(process_model) processor = ProcessInstanceProcessor(process_instance) @@ -44,8 +40,6 @@ class TestRestrictedScriptEngine(BaseTest): bpmn_file_name="read_env.bpmn", process_model_source_directory="dangerous-scripts", ) - self.find_or_create_user() - process_instance = self.create_process_instance_from_process_model(process_model) processor = ProcessInstanceProcessor(process_instance) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py index 0bd909a4..a67bfd2d 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py @@ -3,7 +3,6 @@ from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py index fa560e0e..4eecd38c 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py @@ -11,7 +11,6 @@ from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.spec_reference import SpecReferenceCache -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.spec_file_service import ( ProcessModelFileInvalidError, @@ -39,7 +38,7 @@ class TestSpecFileService(BaseTest): load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - process_model_source_directory='call_activity_nested', + process_model_source_directory="call_activity_nested", ) bpmn_process_id_lookups = SpecReferenceCache.query.all() assert len(bpmn_process_id_lookups) == 1 @@ -57,7 +56,7 @@ class TestSpecFileService(BaseTest): load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - process_model_source_directory='call_activity_nested', + process_model_source_directory="call_activity_nested", ) bpmn_process_id_lookups = SpecReferenceCache.query.all() assert len(bpmn_process_id_lookups) == 1 @@ -94,7 +93,7 @@ class TestSpecFileService(BaseTest): load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - process_model_source_directory='call_activity_nested', + process_model_source_directory="call_activity_nested", ) bpmn_process_id_lookups = SpecReferenceCache.query.all() @@ -144,7 +143,7 @@ class TestSpecFileService(BaseTest): load_test_spec( process_model_id=self.process_model_id, bpmn_file_name=self.bpmn_file_name, - process_model_source_directory='call_activity_nested', + process_model_source_directory="call_activity_nested", ) bpmn_process_id_lookups = SpecReferenceCache.query.all() @@ -173,7 +172,7 @@ class TestSpecFileService(BaseTest): process_model_id = "test_group/call_activity_nested" process_model = load_test_spec( process_model_id=process_model_id, - process_model_source_directory='call_activity_nested', + process_model_source_directory="call_activity_nested", ) files = SpecFileService.get_files(process_model) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py index d13f71c1..5d840ccb 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_various_bpmn_constructs.py @@ -1,14 +1,12 @@ """Test_various_bpmn_constructs.""" from flask.app import Flask -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from flask.testing import FlaskClient from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) -from spiffworkflow_backend.services.process_model_service import ProcessModelService class TestVariousBpmnConstructs(BaseTest): @@ -20,7 +18,7 @@ class TestVariousBpmnConstructs(BaseTest): ) -> None: process_model = load_test_spec( process_model_id="test_group/timer_intermediate_catch_event", - process_model_source_directory='timer_intermediate_catch_event', + process_model_source_directory="timer_intermediate_catch_event", ) process_instance = self.create_process_instance_from_process_model(process_model) processor = ProcessInstanceProcessor(process_instance) From 696bfe224cbd4c607733948abe09472e16129f62 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 22 May 2023 14:58:51 -0400 Subject: [PATCH 37/60] fixed tests related to new permissions w/ burnettk --- spiffworkflow-backend/bin/get_perms | 19 ++++++- spiffworkflow-backend/conftest.py | 5 +- .../src/spiffworkflow_backend/api.yml | 2 +- .../save_process_instance_metadata.bpmn | 52 +++++++++++++++++++ .../helpers/base_test.py | 8 +-- .../integration/test_logging_service.py | 4 -- .../integration/test_process_api.py | 34 ++++++++---- .../routes/ProcessModelNewExperimental.tsx | 2 +- 8 files changed, 101 insertions(+), 25 deletions(-) create mode 100644 spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn diff --git a/spiffworkflow-backend/bin/get_perms b/spiffworkflow-backend/bin/get_perms index 5e0dbd6d..9199efbc 100755 --- a/spiffworkflow-backend/bin/get_perms +++ b/spiffworkflow-backend/bin/get_perms @@ -7,5 +7,20 @@ function error_handler() { trap 'error_handler ${LINENO} $?' ERR set -o errtrace -o errexit -o nounset -o pipefail -set -x -mysql -uroot spiffworkflow_backend_development -e 'select pa.id, g.identifier group_identifier, pt.uri, permission from permission_assignment pa join principal p on p.id = pa.principal_id join `group` g on g.id = p.group_id join permission_target pt on pt.id = pa.permission_target_id;' +database=spiffworkflow_backend_local_development +if [[ "${1:-}" == "test" ]]; then + database=spiffworkflow_backend_unit_testing +fi + +# shellcheck disable=2016 +mysql -uroot "$database" -e ' + select u.username user, g.identifier group + FROM `user` u + JOIN `user_group_assignment` uga on uga.user_id = u.id + JOIN `group` g on g.id = uga.group_id; + + select pa.id, g.identifier group_identifier, pt.uri, permission from permission_assignment pa + join principal p on p.id = pa.principal_id + join `group` g on g.id = p.group_id + join permission_target pt on pt.id = pa.permission_target_id; +' diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index e7102b00..dc0cf650 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -9,6 +9,7 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.process_model_service import ProcessModelService @@ -55,4 +56,6 @@ def with_db_and_bpmn_file_cleanup() -> None: @pytest.fixture() def with_super_admin_user() -> UserModel: """With_super_admin_user.""" - return BaseTest.create_user_with_permission("super_admin") + user = BaseTest.find_or_create_user(username="testadmin1") + AuthorizationService.import_permissions_from_yaml_file(user) + return user diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 8eda4b9f..fd308f44 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -425,7 +425,7 @@ paths: schema: $ref: "#/components/schemas/ProcessModel" - /process-models-natural-language/{modified_process_group_id}: + /process-model-natural-language/{modified_process_group_id}: parameters: - name: modified_process_group_id in: path diff --git a/spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn b/spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn new file mode 100644 index 00000000..f9488b0f --- /dev/null +++ b/spiffworkflow-backend/tests/data/save_process_instance_metadata/save_process_instance_metadata.bpmn @@ -0,0 +1,52 @@ + + + + + Flow_1j4jzft + + + + Flow_01xr2ac + + + Flow_1j4jzft + Flow_10xyk22 + key1 = "value1" + + + + Flow_10xyk22 + Flow_01xr2ac + key2 = "value2"; key3 = "value3" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 46970453..c2768d2d 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -59,7 +59,6 @@ class BaseTest: @staticmethod def logged_in_headers(user: UserModel, _redirect_url: str = "http://some/frontend/url") -> Dict[str, str]: - """Logged_in_headers.""" return dict(Authorization="Bearer " + user.encode_auth_token()) def create_group_and_model_with_bpmn( @@ -317,11 +316,8 @@ class BaseTest: target_uri: str = PermissionTargetModel.URI_ALL, permission_names: Optional[list[str]] = None, ) -> UserModel: - # user = BaseTest.find_or_create_user(username=username) - # return cls.add_permissions_to_user(user, target_uri=target_uri, permission_names=permission_names) - user = BaseTest.find_or_create_user(username="testadmin1") - AuthorizationService.import_permissions_from_yaml_file(user) - return user + user = BaseTest.find_or_create_user(username=username) + return cls.add_permissions_to_user(user, target_uri=target_uri, permission_names=permission_names) @classmethod def add_permissions_to_user( diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py index 6e45386b..eb0ab2ca 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py @@ -25,14 +25,12 @@ class TestLoggingService(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") assert initiator_user.principal is not None AuthorizationService.import_permissions_from_yaml_file() process_model = load_test_spec( process_model_id="misc/category_number_one/simple_form", - # bpmn_file_name="simp.bpmn", process_model_source_directory="simple_form", ) process_instance = self.create_process_instance_from_process_model( @@ -85,14 +83,12 @@ class TestLoggingService(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") initiator_user = self.find_or_create_user("initiator_user") assert initiator_user.principal is not None AuthorizationService.import_permissions_from_yaml_file() process_model = load_test_spec( process_model_id="misc/category_number_one/simple_form", - # bpmn_file_name="simp.bpmn", process_model_source_directory="simple_form", ) process_instance = self.create_process_instance_from_process_model( 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 533bd9a4..a66d165b 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -182,7 +182,7 @@ class TestProcessApi(BaseTest): user=with_super_admin_user, ) response = client.post( - f"/v1.0/process-models-natural-language/{process_group_id}", + f"/v1.0/process-model-natural-language/{process_group_id}", content_type="application/json", data=json.dumps(body), headers=self.logged_in_headers(with_super_admin_user), @@ -238,9 +238,6 @@ class TestProcessApi(BaseTest): process_model_identifier = f"{process_group_id}/{process_model_id}" initial_primary_process_id = "sample" terminal_primary_process_id = "new_process_id" - self.create_process_group_with_api( - client=client, user=with_super_admin_user, process_group_id=process_group_id - ) bpmn_file_name = f"{process_model_id}.bpmn" bpmn_file_source_directory = process_model_id @@ -282,14 +279,11 @@ class TestProcessApi(BaseTest): ) -> None: """Test_process_model_delete.""" process_group_id = "test_process_group" - process_group_description = "Test Process Group" process_model_id = "sample" process_model_identifier = f"{process_group_id}/{process_model_id}" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_description) - self.create_process_model_with_api( - client, + process_model = load_test_spec( process_model_id=process_model_identifier, - user=with_super_admin_user, + process_model_source_directory=process_model_id, ) # assert we have a model @@ -3051,6 +3045,17 @@ class TestProcessApi(BaseTest): bpmn_file_name="save_process_instance_metadata.bpmn", process_model_source_directory="save_process_instance_metadata", ) + ProcessModelService.update_process_model( + process_model, + { + "metadata_extraction_paths": [ + {"key": "key1", "path": "key1"}, + {"key": "key2", "path": "key2"}, + {"key": "key3", "path": "key3"}, + ] + }, + ) + process_instance = self.create_process_instance_from_process_model( process_model=process_model, user=with_super_admin_user ) @@ -3188,6 +3193,16 @@ class TestProcessApi(BaseTest): bpmn_file_name="save_process_instance_metadata.bpmn", process_model_source_directory="save_process_instance_metadata", ) + ProcessModelService.update_process_model( + process_model, + { + "metadata_extraction_paths": [ + {"key": "key1", "path": "key1"}, + {"key": "key2", "path": "key2"}, + {"key": "key3", "path": "key3"}, + ] + }, + ) process_instance = self.create_process_instance_from_process_model( process_model=process_model, user=with_super_admin_user ) @@ -3262,7 +3277,6 @@ class TestProcessApi(BaseTest): with_super_admin_user: UserModel, ) -> None: """Test_process_instance_list_can_order_by_metadata.""" - self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group") process_model = load_test_spec( "test_group/hello_world", process_model_source_directory="nested-task-data-structure", diff --git a/spiffworkflow-frontend/src/routes/ProcessModelNewExperimental.tsx b/spiffworkflow-frontend/src/routes/ProcessModelNewExperimental.tsx index af8be822..e3ef0571 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelNewExperimental.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelNewExperimental.tsx @@ -27,7 +27,7 @@ export default function ProcessModelNewExperimental() { const handleFormSubmission = (event: any) => { event.preventDefault(); HttpService.makeCallToBackend({ - path: `/process-models-natural-language/${params.process_group_id}`, + path: `/process-model-natural-language/${params.process_group_id}`, successCallback: navigateToProcessModel, httpMethod: 'POST', postBody: { natural_language_text: processModelDescriptiveText }, From 2ec53b39c016a6a4ad06feec6e2c5ce3707017ff Mon Sep 17 00:00:00 2001 From: Phillana26 <134319064+Phillana26@users.noreply.github.com> Date: Mon, 22 May 2023 22:09:35 +0200 Subject: [PATCH 38/60] Phillana Test --- docs/documentation/documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/documentation.md b/docs/documentation/documentation.md index cd092b9e..a83024a7 100644 --- a/docs/documentation/documentation.md +++ b/docs/documentation/documentation.md @@ -135,7 +135,7 @@ You can do that by following these steps: 1. Open your browser and go to [http://127.0.0.1:8000](http://127.0.0.1:8000). -### Step 7: Make a change +### Step 7: Make a change to a markdown file 1. Open up a markdown file, and make a change. From 57dbf4d03bd5f506d2b5e91a5feb9b8214edf7a0 Mon Sep 17 00:00:00 2001 From: Phillana26 <134319064+Phillana26@users.noreply.github.com> Date: Mon, 22 May 2023 22:13:37 +0200 Subject: [PATCH 39/60] Phillana Undo --- docs/documentation/documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/documentation.md b/docs/documentation/documentation.md index a83024a7..eb3c9abc 100644 --- a/docs/documentation/documentation.md +++ b/docs/documentation/documentation.md @@ -135,7 +135,7 @@ You can do that by following these steps: 1. Open your browser and go to [http://127.0.0.1:8000](http://127.0.0.1:8000). -### Step 7: Make a change to a markdown file +### Step 7: Make a change 1. Open up a markdown file, and make a change. From 4ed43e505877cbc838a3e9a52ee13d82f62bf450 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 22 May 2023 17:36:07 -0400 Subject: [PATCH 40/60] added api to run process model unit tests w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 42 +++++++++ .../routes/process_models_controller.py | 25 +++++ .../process_model_test_runner_service.py | 81 ++++++++++------ .../multiple-test-files/a.bpmn | 39 ++++++++ .../multiple-test-files/b.bpmn | 42 +++++++++ .../multiple-test-files/process_model.json | 11 +++ .../multiple-test-files/test_a.json | 5 + .../multiple-test-files/test_b.json | 8 ++ .../unit/test_process_model_test_runner.py | 23 ++++- .../src/components/ProcessModelTestRun.tsx | 93 +++++++++++++++++++ .../src/hooks/UriListForPermissions.tsx | 1 + spiffworkflow-frontend/src/index.css | 6 ++ .../src/routes/ProcessModelShow.tsx | 27 ++++-- 13 files changed, 365 insertions(+), 38 deletions(-) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json create mode 100644 spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index fd308f44..fed169ec 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -451,6 +451,48 @@ paths: schema: $ref: "#/components/schemas/ProcessModel" + /process-model-tests/{modified_process_model_identifier}: + parameters: + - name: modified_process_model_identifier + in: path + required: true + description: The process_model_id, modified to replace slashes (/) + schema: + type: string + - name: test_case_file + in: query + required: false + description: The name of the test case file to run + schema: + type: string + - name: test_case_identifier + in: query + required: false + description: The name of the test case file to run + schema: + type: string + post: + operationId: spiffworkflow_backend.routes.process_models_controller.process_model_test_run + summary: Run a test for a process model + tags: + - Process Model Tests + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + "201": + description: Metadata about the uploaded file, but not the file content. + content: + application/json: + schema: + $ref: "#/components/schemas/File" + /process-models/{modified_process_model_identifier}/files: parameters: - name: modified_process_model_identifier 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 00d82639..81e64dce 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -1,5 +1,7 @@ """APIs for dealing with process groups, process models, and process instances.""" import json +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner import os import re from hashlib import sha256 @@ -314,6 +316,29 @@ def process_model_file_show(modified_process_model_identifier: str, file_name: s return make_response(jsonify(file), 200) +def process_model_test_run( + modified_process_model_identifier: str, + test_case_file: Optional[str] = None, + test_case_identifier: Optional[str] = None, +) -> flask.wrappers.Response: + process_model_identifier = modified_process_model_identifier.replace(":", "/") + process_model = _get_process_model(process_model_identifier) + process_model_test_runner = ProcessModelTestRunner( + process_model_directory_path=FileSystemService.root_path(), + process_model_directory_for_test_discovery=FileSystemService.full_path_from_id(process_model.id), + test_case_file=test_case_file, + test_case_identifier=test_case_identifier, + ) + process_model_test_runner.run() + + response_json = { + "all_passed": process_model_test_runner.all_test_cases_passed(), + "passing": process_model_test_runner.passing_tests(), + "failing": process_model_test_runner.failing_tests(), + } + return make_response(jsonify(response_json), 200) + + # { # "natural_language_text": "Create a bug tracker process model \ # with a bug-details form that collects summary, description, and priority" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 47875e10..01057d44 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -47,7 +47,7 @@ class MissingInputTaskData(Exception): class TestCaseResult: passed: bool bpmn_file: str - test_case_name: str + test_case_identifier: str error_messages: Optional[list[str]] = None @@ -90,6 +90,8 @@ class ProcessModelTestRunner: instantiate_executer_callback: Optional[Callable[[str], Any]] = None, execute_task_callback: Optional[Callable[[Any, str, Optional[dict]], Any]] = None, get_next_task_callback: Optional[Callable[[Any], Any]] = None, + test_case_file: Optional[str] = None, + test_case_identifier: Optional[str] = None, ) -> None: self.process_model_directory_path = process_model_directory_path self.process_model_directory_for_test_discovery = ( @@ -98,6 +100,8 @@ class ProcessModelTestRunner: self.instantiate_executer_callback = instantiate_executer_callback self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback + self.test_case_file = test_case_file + self.test_case_identifier = test_case_identifier # keep track of the current task data index self.task_data_index: dict[str, int] = {} @@ -116,13 +120,16 @@ class ProcessModelTestRunner: def failing_tests(self) -> list[TestCaseResult]: return [t for t in self.test_case_results if t.passed is False] + def passing_tests(self) -> list[TestCaseResult]: + return [t for t in self.test_case_results if t.passed is True] + def failing_tests_formatted(self) -> str: formatted_tests = ["FAILING TESTS:"] for failing_test in self.failing_tests(): msg = "" if failing_test.error_messages: msg = "\n\t\t".join(failing_test.error_messages) - formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}") + formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_identifier}: {msg}") return "\n".join(formatted_tests) def run(self) -> None: @@ -134,15 +141,16 @@ class ProcessModelTestRunner: with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) - for test_case_name, test_case_contents in json_file_contents.items(): - self.task_data_index = {} - try: - self.run_test_case(bpmn_file, test_case_name, test_case_contents) - except Exception as ex: - ex_as_array = str(ex).split("\n") - self._add_test_result(False, bpmn_file, test_case_name, ex_as_array) + for test_case_identifier, test_case_contents in json_file_contents.items(): + if self.test_case_identifier is None or test_case_identifier == self.test_case_identifier: + self.task_data_index = {} + try: + self.run_test_case(bpmn_file, test_case_identifier, test_case_contents) + except Exception as ex: + ex_as_array = str(ex).split("\n") + self._add_test_result(False, bpmn_file, test_case_identifier, ex_as_array) - def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: + def run_test_case(self, bpmn_file: str, test_case_identifier: str, test_case_contents: dict) -> None: bpmn_process_instance = self._instantiate_executer(bpmn_file) next_task = self._get_next_task(bpmn_process_instance) while next_task is not None: @@ -158,7 +166,7 @@ class ProcessModelTestRunner: task_type = next_task.task_spec.__class__.__name__ if task_type in ["ServiceTask", "UserTask", "CallActivity"] and test_case_task_properties is None: raise UnrunnableTestCaseError( - f"Cannot run test case '{test_case_name}'. It requires task data for" + f"Cannot run test case '{test_case_identifier}'. It requires task data for" f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) self._execute_task(next_task, test_case_task_key, test_case_task_properties) @@ -183,7 +191,7 @@ class ProcessModelTestRunner: f"expected: {test_case_contents['expected_output_json']}", f"actual: {bpmn_process_instance.data}", ] - self._add_test_result(error_message is None, bpmn_file, test_case_name, error_message) + self._add_test_result(error_message is None, bpmn_file, test_case_identifier, error_message) def _discover_process_model_test_cases( self, @@ -196,14 +204,15 @@ class ProcessModelTestRunner: file_norm = os.path.normpath(file) file_dir = os.path.dirname(file_norm) json_file_name = os.path.basename(file_norm) - bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) - bpmn_file_path = os.path.join(file_dir, bpmn_file_name) - if os.path.isfile(bpmn_file_path): - test_mappings[file_norm] = bpmn_file_path - else: - raise MissingBpmnFileForTestCaseError( - f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" - ) + if self.test_case_file is None or json_file_name == self.test_case_file: + bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) + bpmn_file_path = os.path.join(file_dir, bpmn_file_name) + if os.path.isfile(bpmn_file_path): + test_mappings[file_norm] = bpmn_file_path + else: + raise MissingBpmnFileForTestCaseError( + f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" + ) return test_mappings def _discover_process_model_processes( @@ -218,14 +227,23 @@ class ProcessModelTestRunner: with open(file_norm, "rb") as f: file_contents = f.read() etree_xml_parser = etree.XMLParser(resolve_entities=False) - root = etree.fromstring(file_contents, parser=etree_xml_parser) + + # if we cannot load process model then ignore it since it can cause errors unrelated + # to the test and if it is related, it will most likely be caught further along the test + try: + root = etree.fromstring(file_contents, parser=etree_xml_parser) + except etree.XMLSyntaxError: + continue + call_activities = root.findall(".//bpmn:callActivity", namespaces=DEFAULT_NSMAP) for call_activity in call_activities: - called_element = call_activity.attrib["calledElement"] - self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) + if "calledElement" in call_activity.attrib: + called_element = call_activity.attrib["calledElement"] + self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) - bpmn_process_identifier = bpmn_process_element.attrib["id"] - self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm + if bpmn_process_element is not None: + bpmn_process_identifier = bpmn_process_element.attrib["id"] + self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm def _execute_task( self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] @@ -312,13 +330,13 @@ class ProcessModelTestRunner: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) def _add_test_result( - self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None + self, passed: bool, bpmn_file: str, test_case_identifier: str, error_messages: Optional[list[str]] = None ) -> None: bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file) test_result = TestCaseResult( passed=passed, bpmn_file=bpmn_file_relative, - test_case_name=test_case_name, + test_case_identifier=test_case_identifier, error_messages=error_messages, ) self.test_case_results.append(test_result) @@ -329,9 +347,16 @@ class BpmnFileMissingExecutableProcessError(Exception): class ProcessModelTestRunnerService: - def __init__(self, process_model_directory_path: str) -> None: + def __init__( + self, + process_model_directory_path: str, + test_case_file: Optional[str] = None, + test_case_identifier: Optional[str] = None, + ) -> None: self.process_model_test_runner = ProcessModelTestRunner( process_model_directory_path, + test_case_file=test_case_file, + test_case_identifier=test_case_identifier, # instantiate_executer_callback=self._instantiate_executer_callback, # execute_task_callback=self._execute_task_callback, # get_next_task_callback=self._get_next_task_callback, diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn new file mode 100644 index 00000000..6eb2e331 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn @@ -0,0 +1,39 @@ + + + + + Flow_0jk46kf + + + + Flow_0pw6euz + + + + Flow_0jk46kf + Flow_0pw6euz + a = 1 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn new file mode 100644 index 00000000..33eaa608 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn @@ -0,0 +1,42 @@ + + + + + Flow_1qgv480 + + + + Flow_1sbj39z + + + + Flow_1qgv480 + Flow_1sbj39z + b = 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json new file mode 100644 index 00000000..f8d1350e --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Multiple Test Files", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "a.bpmn", + "primary_process_id": "ProcessA" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json new file mode 100644 index 00000000..756b774e --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json @@ -0,0 +1,5 @@ +{ + "test_case_1": { + "expected_output_json": { "a": 1 } + } +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json new file mode 100644 index 00000000..0cc7cf5f --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json @@ -0,0 +1,8 @@ +{ + "test_case_1": { + "expected_output_json": { "b": 1 } + }, + "test_case_2": { + "expected_output_json": { "b": 1 } + } +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index 53abc61c..c0fc9200 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -53,6 +53,24 @@ class TestProcessModelTestRunner(BaseTest): process_model_test_runner = self._run_model_tests(parent_directory="expected-to-fail") assert len(process_model_test_runner.test_case_results) == 1 + def test_can_test_process_model_with_multiple_files( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files") + assert len(process_model_test_runner.test_case_results) == 3 + + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files", test_case_file='test_a.json') + assert len(process_model_test_runner.test_case_results) == 1 + + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files", test_case_file='test_b.json') + assert len(process_model_test_runner.test_case_results) == 2 + + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files", test_case_file='test_b.json', test_case_identifier='test_case_2') + assert len(process_model_test_runner.test_case_results) == 1 + def test_can_test_process_model_call_activity( self, app: Flask, @@ -81,7 +99,8 @@ class TestProcessModelTestRunner(BaseTest): assert len(process_model_test_runner.test_case_results) == 1 def _run_model_tests( - self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "expected-to-pass" + self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "expected-to-pass", + test_case_file: Optional[str] = None, test_case_identifier: Optional[str] = None, ) -> ProcessModelTestRunner: base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments @@ -90,6 +109,8 @@ class TestProcessModelTestRunner(BaseTest): process_model_test_runner = ProcessModelTestRunner( process_model_directory_path=os.path.join(*base_process_model_dir_path_segments), process_model_directory_for_test_discovery=os.path.join(*path_segments), + test_case_file=test_case_file, + test_case_identifier=test_case_identifier, ) process_model_test_runner.run() diff --git a/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx b/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx new file mode 100644 index 00000000..c6747857 --- /dev/null +++ b/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx @@ -0,0 +1,93 @@ +import { PlayOutline, Checkmark, Close } from '@carbon/icons-react'; +import { Button, Modal } from '@carbon/react'; +import { useState } from 'react'; +import { useUriListForPermissions } from '../hooks/UriListForPermissions'; +import HttpService from '../services/HttpService'; +import { ProcessFile } from '../interfaces'; + +type OwnProps = { + processModelFile: ProcessFile; +}; + +export default function ProcessModelTestRun({ processModelFile }: OwnProps) { + const [testCaseResults, setTestCaseResults] = useState(null); + const [showTestCaseResultsModal, setShowTestCaseResultsModal] = + useState(false); + const { targetUris } = useUriListForPermissions(); + + const onProcessModelTestRunSuccess = (result: any) => { + setTestCaseResults(result); + }; + + const processModelTestRunResultTag = () => { + if (testCaseResults) { + if (testCaseResults.all_passed) { + return ( + + ); + } + return null; + }; + return ( <> {testCaseResultsModal()} - + + {hasTestCaseFiles ? ( + + ) : null} + {processModelFilesSection()} From ac73ee47f6db82a2668ad2d47a05e13116a825ad Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 23 May 2023 15:50:55 -0400 Subject: [PATCH 53/60] fixed tests failing for typeguard w/ burnettk --- .../process_model_test_runner_service.py | 13 +++++++---- .../unit/test_process_model_test_runner.py | 23 ++++--------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 4b833139..fc683ff5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -1,5 +1,6 @@ import glob import json +from typing import Union import os import re import traceback @@ -89,6 +90,8 @@ JSON file format: } } """ + + class ProcessModelTestRunner: """Generic test runner code. May move into own library at some point. @@ -257,7 +260,7 @@ class ProcessModelTestRunner: self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm def _execute_task( - self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + self, spiff_task: SpiffTask, test_case_task_key: Optional[str], test_case_task_properties: Optional[dict] ) -> None: if self.execute_task_callback: self.execute_task_callback(spiff_task, test_case_task_key, test_case_task_properties) @@ -280,10 +283,10 @@ class ProcessModelTestRunner: return None def _default_execute_task( - self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + self, spiff_task: SpiffTask, test_case_task_key: Optional[str], test_case_task_properties: Optional[dict] ) -> None: if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == "ServiceTask": - if test_case_task_properties and "data" in test_case_task_properties: + if test_case_task_key and test_case_task_properties and "data" in test_case_task_properties: if test_case_task_key not in self.task_data_index: self.task_data_index[test_case_task_key] = 0 task_data_length = len(test_case_task_properties["data"]) @@ -309,7 +312,7 @@ class ProcessModelTestRunner: related_bpmn_files.extend(self._find_related_bpmn_files(new_file)) return related_bpmn_files - def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree.Element: + def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree._Element: data = None with open(bpmn_file, "rb") as f_handle: data = f_handle.read() @@ -340,7 +343,7 @@ class ProcessModelTestRunner: def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) - def _exception_to_test_case_error_details(self, exception: Exception) -> TestCaseErrorDetails: + def _exception_to_test_case_error_details(self, exception: Union[Exception, WorkflowTaskException]) -> TestCaseErrorDetails: error_messages = str(exception).split("\n") test_case_error_details = TestCaseErrorDetails(error_messages=error_messages) if isinstance(exception, WorkflowTaskException): diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index 094cf777..bdf8633a 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -1,15 +1,12 @@ import os -from typing import Any +# from typing import Any from typing import Optional import pytest from flask import current_app from flask import Flask -from pytest_mock import MockerFixture from tests.spiffworkflow_backend.helpers.base_test import BaseTest -from spiffworkflow_backend.models.task import TaskModel # noqa: F401 -from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner @@ -19,7 +16,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests("script-task") assert len(process_model_test_runner.test_case_results) == 1 @@ -28,9 +24,8 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: - process_model_test_runner = ProcessModelTestRunner(os.path.join(FileSystemService.root_path(), "DNE")) + process_model_test_runner = ProcessModelTestRunner(os.path.join(self.root_path(), "DNE")) with pytest.raises(NoTestCasesFoundError): process_model_test_runner.run() assert process_model_test_runner.all_test_cases_passed(), process_model_test_runner.test_case_results @@ -39,7 +34,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests() assert len(process_model_test_runner.test_case_results) > 1 @@ -48,7 +42,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(parent_directory="expected-to-fail") assert len(process_model_test_runner.test_case_results) == 1 @@ -57,7 +50,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files") assert len(process_model_test_runner.test_case_results) == 3 @@ -83,7 +75,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="call-activity") assert len(process_model_test_runner.test_case_results) == 1 @@ -92,7 +83,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="service-task") assert len(process_model_test_runner.test_case_results) == 1 @@ -101,7 +91,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="loopback-to-user-task") assert len(process_model_test_runner.test_case_results) == 1 @@ -113,7 +102,7 @@ class TestProcessModelTestRunner(BaseTest): test_case_file: Optional[str] = None, test_case_identifier: Optional[str] = None, ) -> ProcessModelTestRunner: - base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] + base_process_model_dir_path_segments = [self.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments if bpmn_process_directory_name: path_segments = path_segments + [bpmn_process_directory_name] @@ -131,9 +120,8 @@ class TestProcessModelTestRunner(BaseTest): ), process_model_test_runner.failing_tests_formatted() return process_model_test_runner - @pytest.fixture() - def with_mocked_root_path(self, mocker: MockerFixture) -> None: - path = os.path.join( + def root_path(self) -> str: + return os.path.join( current_app.instance_path, "..", "..", @@ -141,4 +129,3 @@ class TestProcessModelTestRunner(BaseTest): "data", "bpmn_unit_test_process_models", ) - mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) From 5e25e591ae8acc8d3f06ba8f097dbda2f878c422 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 23 May 2023 15:55:27 -0400 Subject: [PATCH 54/60] pyl w/ burnettk --- .../services/process_model_test_runner_service.py | 8 +++++--- .../unit/test_process_model_test_runner.py | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index fc683ff5..db5a0d44 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -1,6 +1,5 @@ import glob import json -from typing import Union import os import re import traceback @@ -8,6 +7,7 @@ from dataclasses import dataclass from typing import Any from typing import Callable from typing import Optional +from typing import Union from lxml import etree # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore @@ -103,7 +103,7 @@ class ProcessModelTestRunner: process_model_directory_path: str, process_model_directory_for_test_discovery: Optional[str] = None, instantiate_executer_callback: Optional[Callable[[str], Any]] = None, - execute_task_callback: Optional[Callable[[Any, str, Optional[dict]], Any]] = None, + execute_task_callback: Optional[Callable[[Any, Optional[str], Optional[dict]], Any]] = None, get_next_task_callback: Optional[Callable[[Any], Any]] = None, test_case_file: Optional[str] = None, test_case_identifier: Optional[str] = None, @@ -343,7 +343,9 @@ class ProcessModelTestRunner: def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) - def _exception_to_test_case_error_details(self, exception: Union[Exception, WorkflowTaskException]) -> TestCaseErrorDetails: + def _exception_to_test_case_error_details( + self, exception: Union[Exception, WorkflowTaskException] + ) -> TestCaseErrorDetails: error_messages = str(exception).split("\n") test_case_error_details = TestCaseErrorDetails(error_messages=error_messages) if isinstance(exception, WorkflowTaskException): diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index bdf8633a..78e6b2e1 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -1,5 +1,4 @@ import os -# from typing import Any from typing import Optional import pytest From 68af0ad1dc06adbbe5200f7ad7f0a4066ab22d64 Mon Sep 17 00:00:00 2001 From: burnettk Date: Tue, 23 May 2023 18:07:57 -0400 Subject: [PATCH 55/60] add docs on BPMN unit tests --- docs/how_to/bpmn_unit_tests.md | 48 +++++++++++++++++++++++++++++++++ docs/index.md | 7 +++-- docs/quick_start/quick_start.md | 2 +- 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 docs/how_to/bpmn_unit_tests.md diff --git a/docs/how_to/bpmn_unit_tests.md b/docs/how_to/bpmn_unit_tests.md new file mode 100644 index 00000000..5f1c9547 --- /dev/null +++ b/docs/how_to/bpmn_unit_tests.md @@ -0,0 +1,48 @@ +# BPMN Unit Tests + +Software Engineers test their code. +With this feature, BPMN authors can test their creations, too. +These tests can provide you with faster feedback than you would get by simply running your process model, and they allow you to mock out form input and service task connections as well as provide specific input to exercise different branches of your process model. +BPMN unit tests are designed to give you greater confidence that your process models will work as designed when they are run in the wild, both the first time they are used by real users and also after you make changes to them. + +## Creating BPMN Unit Tests + +First, create a process model that you want to test. +Navigate to the process model and add a JSON file based on the name of one of the BPMN files. +For example, if you have a process model that includes a file called `awesome_script_task.bpmn`, your test JSON file would be called `test_awesome_script_task.json`. +If you have multiple BPMN files you want to test, you can have multiple test JSON files. +The BPMN files you test do not have to be marked as the primary file for the process model in question. +The structure of your json should be as follows: + + { + "test_case_1": { + "tasks": { + "ServiceTaskProcess:service_task_one": { + "data": [{ "the_result": "result_from_service" }] + } + }, + "expected_output_json": { "the_result": "result_from_service" } + } + } + +The top-level keys should be names of unit tests. +In this example, the unit test is named "test_case_1." +Under that, you can specify "tasks" and "expected_output_json." + +Under "tasks," each key is the BPMN id of a specific task. +If you are testing a file that uses Call Activities and therefore calls other processes, there can be conflicting BPMN ids. +In this case, you can specify the unique activity by prepending the Process id (in the above example, that is "ServiceTaskProcess"). +For simple processes, "service_task_one" (for example) would be sufficient as the BPMN id. +For User Tasks, the "data" (under a specific task) represents the data that will be entered by the user in the form. +For Service Tasks, the data represents the data that will be returned by the service. +Note that all User Tasks and Service Tasks must have their BPMN ids mentioned in the JSON file (with mock task data as desired), since otherwise we won't know what to do when the flow arrives at these types of tasks. + +The "expected_output_json" represents the state of the task data that you expect when the process completes. +When the test is run, if the actual task data differs from this expectation, the test will fail. +The test will also fail if the process never completes or if an error occurs. + +## Running BPMN Unit Tests + +Go to a process model and either click “Run Unit Tests” to run all tests for the process model or click on the “play icon” next to a "test_something.json" file. +Then you will get a green check mark or a red x. +You can click on these colored icons to get more details about the passing or failing test. diff --git a/docs/index.md b/docs/index.md index c5442c2e..599b6d71 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,16 @@ -Welcome to SpiffWorkflow's documentation! -======================================= +# Welcome to SpiffWorkflow's documentation ```{toctree} :maxdepth: 2 :caption: Contents quick_start/quick_start.md documentation/documentation.md +how_to/bpmn_unit_tests.md ``` This is great! -Indices and tables -================== +## Indices and tables * [](genindex) * [](modindex) diff --git a/docs/quick_start/quick_start.md b/docs/quick_start/quick_start.md index 0ac4c51b..38111ddd 100644 --- a/docs/quick_start/quick_start.md +++ b/docs/quick_start/quick_start.md @@ -307,4 +307,4 @@ Ensure that all required details have been included such as Process name, Proces By following these steps, you can request the special permissions needed to carry out your tasks effectively. -Changes added by Usama \ No newline at end of file +Changes added by Usama From 80f74f239ce2f9011f135fe3ab7775f45659dbab Mon Sep 17 00:00:00 2001 From: usama9500 Date: Wed, 24 May 2023 19:18:41 +0500 Subject: [PATCH 56/60] Text removed --- docs/quick_start/quick_start.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/quick_start/quick_start.md b/docs/quick_start/quick_start.md index 0ac4c51b..cf86b21e 100644 --- a/docs/quick_start/quick_start.md +++ b/docs/quick_start/quick_start.md @@ -305,6 +305,4 @@ Ensure that all required details have been included such as Process name, Proces ![Untitled](images/Untitled_32.png) -By following these steps, you can request the special permissions needed to carry out your tasks effectively. - -Changes added by Usama \ No newline at end of file +By following these steps, you can request the special permissions needed to carry out your tasks effectively. \ No newline at end of file From 8c8c9635d0a9db4721847de69065f8b93a8f293e Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 24 May 2023 10:53:04 -0400 Subject: [PATCH 57/60] allow comparing dates in json schema forms and also allow checking nested fields w/ burnettk --- .../src/routes/TaskShow.tsx | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index 6f1407d5..b50111d1 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -167,6 +167,7 @@ export default function TaskShow() { if (disabled) { return; } + return; const dataToSubmit = formObject?.formData; if (!dataToSubmit) { @@ -270,32 +271,74 @@ export default function TaskShow() { formData: any, errors: any ) => { - if ('properties' in jsonSchema) { - Object.keys(jsonSchema.properties).forEach((propertyKey: string) => { - const propertyMetadata = jsonSchema.properties[propertyKey]; + // if the jsonSchema has an items attribute then assume the element itself + // doesn't have a custom validation but it's children could so use that + const jsonSchemaToUse = + 'items' in jsonSchema ? jsonSchema.items : jsonSchema; + + if ('properties' in jsonSchemaToUse) { + Object.keys(jsonSchemaToUse.properties).forEach((propertyKey: string) => { + const propertyMetadata = jsonSchemaToUse.properties[propertyKey]; if ( typeof propertyMetadata === 'object' && - 'minimumDate' in propertyMetadata && - propertyMetadata.minimumDate === 'today' + 'minimumDate' in propertyMetadata ) { - const dateToday = new Date(); const dateValue = formData[propertyKey]; if (dateValue) { const dateValueObject = new Date(dateValue); const dateValueString = dateValueObject.toISOString().split('T')[0]; - const dateTodayString = dateToday.toISOString().split('T')[0]; - if (dateTodayString > dateValueString) { - errors[propertyKey].addError('must be today or after'); + if (propertyMetadata.minimumDate === 'today') { + const dateToday = new Date(); + const dateTodayString = dateToday.toISOString().split('T')[0]; + if (dateTodayString > dateValueString) { + errors[propertyKey].addError('must be today or after'); + } + } else if (propertyMetadata.minimumDate.startsWith('field:')) { + const fieldIdentifierToCompareWith = propertyMetadata.minimumDate + .split(':') + .slice(1) + .join(':'); + if (fieldIdentifierToCompareWith in formData) { + const dateToCompareWith = + formData[fieldIdentifierToCompareWith]; + const dateObjectToCompareWith = new Date(dateToCompareWith); + const dateStringToCompareWith = dateObjectToCompareWith + .toISOString() + .split('T')[0]; + if (dateToCompareWith) { + if (dateStringToCompareWith > dateValueString) { + errors[propertyKey].addError( + `this field must be equal to greater than '${fieldIdentifierToCompareWith}'` + ); + } + } else { + errors[propertyKey].addError( + `this field was supposed to be compared against '${fieldIdentifierToCompareWith}' but that field did not have a value` + ); + } + } else { + errors[propertyKey].addError( + `this field was supposed to be compared against '${fieldIdentifierToCompareWith}' but that field cannot be found` + ); + } } } } // recurse through all nested properties as well - getFieldsWithDateValidations( - propertyMetadata, - formData[propertyKey], - errors[propertyKey] - ); + let formDataToSend = formData[propertyKey]; + if (formDataToSend) { + if (formDataToSend.constructor.name !== 'Array') { + formDataToSend = [formDataToSend]; + } + formDataToSend.forEach((item: any, index: number) => { + let errorsToSend = errors[propertyKey]; + if (index in errorsToSend) { + errorsToSend = errorsToSend[index]; + } + getFieldsWithDateValidations(propertyMetadata, item, errorsToSend); + }); + } }); } return errors; From 9c0a8cc9dfd2701f16edd63124f2b4ecb5ded165 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 24 May 2023 11:10:40 -0400 Subject: [PATCH 58/60] only test admin permissions in ci since it takes longer run w/ burnettk --- .github/workflows/backend_tests.yml | 1 + spiffworkflow-backend/conftest.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backend_tests.yml b/.github/workflows/backend_tests.yml index 24ff8838..c164e0da 100644 --- a/.github/workflows/backend_tests.yml +++ b/.github/workflows/backend_tests.yml @@ -84,6 +84,7 @@ jobs: PRE_COMMIT_COLOR: "always" SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD: password SPIFFWORKFLOW_BACKEND_DATABASE_TYPE: ${{ matrix.database }} + SPIFFWORKFLOW_BACKEND_RUNNING_IN_CI: 'true' steps: - name: Check out the repository diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index 2cd60296..a6be6423 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -1,4 +1,4 @@ -"""Conftest.""" +# noqa import os import shutil @@ -25,8 +25,7 @@ from spiffworkflow_backend import create_app # noqa: E402 @pytest.fixture(scope="session") -def app() -> Flask: - """App.""" +def app() -> Flask: # noqa os.environ["SPIFFWORKFLOW_BACKEND_ENV"] = "unit_testing" os.environ["FLASK_SESSION_SECRET_KEY"] = "e7711a3ba96c46c68e084a86952de16f" app = create_app() @@ -53,8 +52,12 @@ def with_db_and_bpmn_file_cleanup() -> None: @pytest.fixture() -def with_super_admin_user() -> UserModel: - """With_super_admin_user.""" - user = BaseTest.find_or_create_user(username="testadmin1") - AuthorizationService.import_permissions_from_yaml_file(user) +def with_super_admin_user() -> UserModel: # noqa + # this loads all permissions from yaml everytime this function is called which is slow + # so default to just setting a simple super admin and only run with the "real" permissions in ci + if os.environ.get("SPIFFWORKFLOW_BACKEND_RUNNING_IN_CI") == "true": + user = BaseTest.find_or_create_user(username="testadmin1") + AuthorizationService.import_permissions_from_yaml_file(user) + else: + user = BaseTest.create_user_with_permission("super_admin") return user From de5f1550169a42d32689b96071677f03d5e7a6ba Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 24 May 2023 11:47:02 -0400 Subject: [PATCH 59/60] cleaned up comparing dates in forms w/ burnettk --- .../src/routes/TaskShow.tsx | 114 +++++++++++------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index b50111d1..af8f9446 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -167,7 +167,6 @@ export default function TaskShow() { if (disabled) { return; } - return; const dataToSubmit = formObject?.formData; if (!dataToSubmit) { @@ -266,10 +265,77 @@ export default function TaskShow() { return null; }; + const formatDateString = (dateString?: string) => { + let dateObject = new Date(); + if (dateString) { + dateObject = new Date(dateString); + } + return dateObject.toISOString().split('T')[0]; + }; + + const checkFieldComparisons = ( + formData: any, + propertyKey: string, + propertyMetadata: any, + formattedDateString: string, + errors: any + ) => { + const fieldIdentifierToCompareWith = propertyMetadata.minimumDate.replace( + /^field:/, + '' + ); + if (fieldIdentifierToCompareWith in formData) { + const dateToCompareWith = formData[fieldIdentifierToCompareWith]; + if (dateToCompareWith) { + const dateStringToCompareWith = formatDateString(dateToCompareWith); + if (dateStringToCompareWith > formattedDateString) { + errors[propertyKey].addError( + `must be equal to or greater than '${fieldIdentifierToCompareWith}'` + ); + } + } else { + errors[propertyKey].addError( + `was supposed to be compared against '${fieldIdentifierToCompareWith}' but that field did not have a value` + ); + } + } else { + errors[propertyKey].addError( + `was supposed to be compared against '${fieldIdentifierToCompareWith}' but it either doesn't have a value or does not exist` + ); + } + }; + + const checkMinimumDate = ( + formData: any, + propertyKey: string, + propertyMetadata: any, + errors: any + ) => { + const dateString = formData[propertyKey]; + if (dateString) { + const formattedDateString = formatDateString(dateString); + if (propertyMetadata.minimumDate === 'today') { + const dateTodayString = formatDateString(); + if (dateTodayString > formattedDateString) { + errors[propertyKey].addError('must be today or after'); + } + } else if (propertyMetadata.minimumDate.startsWith('field:')) { + checkFieldComparisons( + formData, + propertyKey, + propertyMetadata, + formattedDateString, + errors + ); + } + } + }; + const getFieldsWithDateValidations = ( jsonSchema: any, formData: any, errors: any + // eslint-disable-next-line sonarjs/cognitive-complexity ) => { // if the jsonSchema has an items attribute then assume the element itself // doesn't have a custom validation but it's children could so use that @@ -279,50 +345,8 @@ export default function TaskShow() { if ('properties' in jsonSchemaToUse) { Object.keys(jsonSchemaToUse.properties).forEach((propertyKey: string) => { const propertyMetadata = jsonSchemaToUse.properties[propertyKey]; - if ( - typeof propertyMetadata === 'object' && - 'minimumDate' in propertyMetadata - ) { - const dateValue = formData[propertyKey]; - if (dateValue) { - const dateValueObject = new Date(dateValue); - const dateValueString = dateValueObject.toISOString().split('T')[0]; - if (propertyMetadata.minimumDate === 'today') { - const dateToday = new Date(); - const dateTodayString = dateToday.toISOString().split('T')[0]; - if (dateTodayString > dateValueString) { - errors[propertyKey].addError('must be today or after'); - } - } else if (propertyMetadata.minimumDate.startsWith('field:')) { - const fieldIdentifierToCompareWith = propertyMetadata.minimumDate - .split(':') - .slice(1) - .join(':'); - if (fieldIdentifierToCompareWith in formData) { - const dateToCompareWith = - formData[fieldIdentifierToCompareWith]; - const dateObjectToCompareWith = new Date(dateToCompareWith); - const dateStringToCompareWith = dateObjectToCompareWith - .toISOString() - .split('T')[0]; - if (dateToCompareWith) { - if (dateStringToCompareWith > dateValueString) { - errors[propertyKey].addError( - `this field must be equal to greater than '${fieldIdentifierToCompareWith}'` - ); - } - } else { - errors[propertyKey].addError( - `this field was supposed to be compared against '${fieldIdentifierToCompareWith}' but that field did not have a value` - ); - } - } else { - errors[propertyKey].addError( - `this field was supposed to be compared against '${fieldIdentifierToCompareWith}' but that field cannot be found` - ); - } - } - } + if ('minimumDate' in propertyMetadata) { + checkMinimumDate(formData, propertyKey, propertyMetadata, errors); } // recurse through all nested properties as well From 89f3dbc7b83ce590ebb0eec931f032d45094b182 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 24 May 2023 15:26:26 -0400 Subject: [PATCH 60/60] moved callback code to delegate class in process model test runner w/ burnettk --- .../process_model_test_runner_service.py | 331 ++++++++++-------- .../unit/test_process_model_test_runner.py | 11 + 2 files changed, 196 insertions(+), 146 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index db5a0d44..70a73f4f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -3,10 +3,10 @@ import json import os import re import traceback +from abc import abstractmethod from dataclasses import dataclass -from typing import Any -from typing import Callable from typing import Optional +from typing import Type from typing import Union from lxml import etree # type: ignore @@ -34,6 +34,14 @@ class MissingInputTaskData(Exception): pass +class UnsupporterRunnerDelegateGiven(Exception): + pass + + +class BpmnFileMissingExecutableProcessError(Exception): + pass + + @dataclass class TestCaseErrorDetails: error_messages: list[str] @@ -53,6 +61,124 @@ class TestCaseResult: test_case_error_details: Optional[TestCaseErrorDetails] = None +class ProcessModelTestRunnerDelegate: + """Abstract class for the process model test runner delegate. + + All delegates MUST inherit from this class. + """ + + def __init__( + self, + process_model_directory_path: str, + ) -> None: + self.process_model_directory_path = process_model_directory_path + + @abstractmethod + def instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: + raise NotImplementedError("method instantiate_executer must be implemented") + + @abstractmethod + def execute_task(self, spiff_task: SpiffTask, task_data_for_submit: Optional[dict] = None) -> None: + raise NotImplementedError("method execute_task must be implemented") + + @abstractmethod + def get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + raise NotImplementedError("method get_next_task must be implemented") + + +class ProcessModelTestRunnerMostlyPureSpiffDelegate(ProcessModelTestRunnerDelegate): + def __init__( + self, + process_model_directory_path: str, + ) -> None: + super().__init__(process_model_directory_path) + self.bpmn_processes_to_file_mappings: dict[str, str] = {} + self.bpmn_files_to_called_element_mappings: dict[str, list[str]] = {} + self._discover_process_model_processes() + + def instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: + parser = MyCustomParser() + bpmn_file_etree = self._get_etree_from_bpmn_file(bpmn_file) + parser.add_bpmn_xml(bpmn_file_etree, filename=os.path.basename(bpmn_file)) + all_related = self._find_related_bpmn_files(bpmn_file) + for related_file in all_related: + related_file_etree = self._get_etree_from_bpmn_file(related_file) + parser.add_bpmn_xml(related_file_etree, filename=os.path.basename(related_file)) + sub_parsers = list(parser.process_parsers.values()) + executable_process = None + for sub_parser in sub_parsers: + if sub_parser.process_executable: + executable_process = sub_parser.bpmn_id + if executable_process is None: + raise BpmnFileMissingExecutableProcessError( + f"Executable process cannot be found in {bpmn_file}. Test cannot run." + ) + bpmn_process_spec = parser.get_spec(executable_process) + bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) + return bpmn_process_instance + + def execute_task(self, spiff_task: SpiffTask, task_data_for_submit: Optional[dict] = None) -> None: + if task_data_for_submit is not None or spiff_task.task_spec.manual: + if task_data_for_submit is not None: + spiff_task.update_data(task_data_for_submit) + spiff_task.complete() + else: + spiff_task.run() + + def get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + ready_tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY)]) + if len(ready_tasks) > 0: + return ready_tasks[0] + return None + + def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree._Element: + data = None + with open(bpmn_file, "rb") as f_handle: + data = f_handle.read() + etree_xml_parser = etree.XMLParser(resolve_entities=False) + return etree.fromstring(data, parser=etree_xml_parser) + + def _find_related_bpmn_files(self, bpmn_file: str) -> list[str]: + related_bpmn_files = [] + if bpmn_file in self.bpmn_files_to_called_element_mappings: + for bpmn_process_identifier in self.bpmn_files_to_called_element_mappings[bpmn_file]: + if bpmn_process_identifier in self.bpmn_processes_to_file_mappings: + new_file = self.bpmn_processes_to_file_mappings[bpmn_process_identifier] + related_bpmn_files.append(new_file) + related_bpmn_files.extend(self._find_related_bpmn_files(new_file)) + return related_bpmn_files + + def _discover_process_model_processes( + self, + ) -> None: + process_model_bpmn_file_glob = os.path.join(self.process_model_directory_path, "**", "*.bpmn") + + for file in glob.glob(process_model_bpmn_file_glob, recursive=True): + file_norm = os.path.normpath(file) + if file_norm not in self.bpmn_files_to_called_element_mappings: + self.bpmn_files_to_called_element_mappings[file_norm] = [] + with open(file_norm, "rb") as f: + file_contents = f.read() + etree_xml_parser = etree.XMLParser(resolve_entities=False) + + # if we cannot load process model then ignore it since it can cause errors unrelated + # to the test and if it is related, it will most likely be caught further along the test + try: + root = etree.fromstring(file_contents, parser=etree_xml_parser) + except etree.XMLSyntaxError: + continue + + call_activities = root.findall(".//bpmn:callActivity", namespaces=DEFAULT_NSMAP) + for call_activity in call_activities: + if "calledElement" in call_activity.attrib: + called_element = call_activity.attrib["calledElement"] + self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) + bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) + if bpmn_process_element is not None: + bpmn_process_identifier = bpmn_process_element.attrib["id"] + self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm + + DEFAULT_NSMAP = { "bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL", "bpmndi": "http://www.omg.org/spec/BPMN/20100524/DI", @@ -93,18 +219,16 @@ JSON file format: class ProcessModelTestRunner: - """Generic test runner code. May move into own library at some point. + """Runs the test case json files for a given process model directory. - KEEP THIS GENERIC. do not add backend specific code here. + It searches for test case files recursively and will run all it finds by default. """ def __init__( self, process_model_directory_path: str, + process_model_test_runner_delegate_class: Type = ProcessModelTestRunnerMostlyPureSpiffDelegate, process_model_directory_for_test_discovery: Optional[str] = None, - instantiate_executer_callback: Optional[Callable[[str], Any]] = None, - execute_task_callback: Optional[Callable[[Any, Optional[str], Optional[dict]], Any]] = None, - get_next_task_callback: Optional[Callable[[Any], Any]] = None, test_case_file: Optional[str] = None, test_case_identifier: Optional[str] = None, ) -> None: @@ -112,21 +236,24 @@ class ProcessModelTestRunner: self.process_model_directory_for_test_discovery = ( process_model_directory_for_test_discovery or process_model_directory_path ) - self.instantiate_executer_callback = instantiate_executer_callback - self.execute_task_callback = execute_task_callback - self.get_next_task_callback = get_next_task_callback self.test_case_file = test_case_file self.test_case_identifier = test_case_identifier - # keep track of the current task data index - self.task_data_index: dict[str, int] = {} + if not issubclass(process_model_test_runner_delegate_class, ProcessModelTestRunnerDelegate): + raise UnsupporterRunnerDelegateGiven( + "Process model test runner delegate must inherit from ProcessModelTestRunnerDelegate. Given" + f" class '{process_model_test_runner_delegate_class}' does not" + ) - self.test_case_results: list[TestCaseResult] = [] - self.bpmn_processes_to_file_mappings: dict[str, str] = {} - self.bpmn_files_to_called_element_mappings: dict[str, list[str]] = {} + self.process_model_test_runner_delegate = process_model_test_runner_delegate_class( + process_model_directory_path + ) self.test_mappings = self._discover_process_model_test_cases() - self._discover_process_model_processes() + self.test_case_results: list[TestCaseResult] = [] + + # keep track of the current task data index + self.task_data_index: dict[str, int] = {} def all_test_cases_passed(self) -> bool: failed_tests = self.failing_tests() @@ -178,7 +305,9 @@ class ProcessModelTestRunner: test_case_task_properties = test_case_contents["tasks"][test_case_task_key] task_type = next_task.task_spec.__class__.__name__ - if task_type in ["ServiceTask", "UserTask", "CallActivity"] and test_case_task_properties is None: + if task_type in ["ServiceTask", "UserTask", "CallActivity"] and ( + test_case_task_properties is None or "data" not in test_case_task_properties + ): raise UnrunnableTestCaseError( f"Cannot run test case '{test_case_identifier}'. It requires task data for" f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" @@ -207,138 +336,29 @@ class ProcessModelTestRunner: ] self._add_test_result(error_message is None, bpmn_file, test_case_identifier, error_message) - def _discover_process_model_test_cases( - self, - ) -> dict[str, str]: - test_mappings = {} - - json_test_file_glob = os.path.join(self.process_model_directory_for_test_discovery, "**", "test_*.json") - - for file in glob.glob(json_test_file_glob, recursive=True): - file_norm = os.path.normpath(file) - file_dir = os.path.dirname(file_norm) - json_file_name = os.path.basename(file_norm) - if self.test_case_file is None or json_file_name == self.test_case_file: - bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) - bpmn_file_path = os.path.join(file_dir, bpmn_file_name) - if os.path.isfile(bpmn_file_path): - test_mappings[file_norm] = bpmn_file_path - else: - raise MissingBpmnFileForTestCaseError( - f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" - ) - return test_mappings - - def _discover_process_model_processes( - self, - ) -> None: - process_model_bpmn_file_glob = os.path.join(self.process_model_directory_path, "**", "*.bpmn") - - for file in glob.glob(process_model_bpmn_file_glob, recursive=True): - file_norm = os.path.normpath(file) - if file_norm not in self.bpmn_files_to_called_element_mappings: - self.bpmn_files_to_called_element_mappings[file_norm] = [] - with open(file_norm, "rb") as f: - file_contents = f.read() - etree_xml_parser = etree.XMLParser(resolve_entities=False) - - # if we cannot load process model then ignore it since it can cause errors unrelated - # to the test and if it is related, it will most likely be caught further along the test - try: - root = etree.fromstring(file_contents, parser=etree_xml_parser) - except etree.XMLSyntaxError: - continue - - call_activities = root.findall(".//bpmn:callActivity", namespaces=DEFAULT_NSMAP) - for call_activity in call_activities: - if "calledElement" in call_activity.attrib: - called_element = call_activity.attrib["calledElement"] - self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) - bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) - if bpmn_process_element is not None: - bpmn_process_identifier = bpmn_process_element.attrib["id"] - self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm - def _execute_task( self, spiff_task: SpiffTask, test_case_task_key: Optional[str], test_case_task_properties: Optional[dict] ) -> None: - if self.execute_task_callback: - self.execute_task_callback(spiff_task, test_case_task_key, test_case_task_properties) - self._default_execute_task(spiff_task, test_case_task_key, test_case_task_properties) + task_data_for_submit = None + if test_case_task_key and test_case_task_properties and "data" in test_case_task_properties: + if test_case_task_key not in self.task_data_index: + self.task_data_index[test_case_task_key] = 0 + task_data_length = len(test_case_task_properties["data"]) + test_case_index = self.task_data_index[test_case_task_key] + if task_data_length <= test_case_index: + raise MissingInputTaskData( + f"Missing input task data for task: {test_case_task_key}. " + f"Only {task_data_length} given in the json but task was called {test_case_index + 1} times" + ) + task_data_for_submit = test_case_task_properties["data"][test_case_index] + self.task_data_index[test_case_task_key] += 1 + self.process_model_test_runner_delegate.execute_task(spiff_task, task_data_for_submit) def _get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: - if self.get_next_task_callback: - return self.get_next_task_callback(bpmn_process_instance) - return self._default_get_next_task(bpmn_process_instance) + return self.process_model_test_runner_delegate.get_next_task(bpmn_process_instance) def _instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: - if self.instantiate_executer_callback: - return self.instantiate_executer_callback(bpmn_file) - return self._default_instantiate_executer(bpmn_file) - - def _default_get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: - ready_tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY)]) - if len(ready_tasks) > 0: - return ready_tasks[0] - return None - - def _default_execute_task( - self, spiff_task: SpiffTask, test_case_task_key: Optional[str], test_case_task_properties: Optional[dict] - ) -> None: - if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == "ServiceTask": - if test_case_task_key and test_case_task_properties and "data" in test_case_task_properties: - if test_case_task_key not in self.task_data_index: - self.task_data_index[test_case_task_key] = 0 - task_data_length = len(test_case_task_properties["data"]) - test_case_index = self.task_data_index[test_case_task_key] - if task_data_length <= test_case_index: - raise MissingInputTaskData( - f"Missing input task data for task: {test_case_task_key}. " - f"Only {task_data_length} given in the json but task was called {test_case_index + 1} times" - ) - spiff_task.update_data(test_case_task_properties["data"][test_case_index]) - self.task_data_index[test_case_task_key] += 1 - spiff_task.complete() - else: - spiff_task.run() - - def _find_related_bpmn_files(self, bpmn_file: str) -> list[str]: - related_bpmn_files = [] - if bpmn_file in self.bpmn_files_to_called_element_mappings: - for bpmn_process_identifier in self.bpmn_files_to_called_element_mappings[bpmn_file]: - if bpmn_process_identifier in self.bpmn_processes_to_file_mappings: - new_file = self.bpmn_processes_to_file_mappings[bpmn_process_identifier] - related_bpmn_files.append(new_file) - related_bpmn_files.extend(self._find_related_bpmn_files(new_file)) - return related_bpmn_files - - def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree._Element: - data = None - with open(bpmn_file, "rb") as f_handle: - data = f_handle.read() - etree_xml_parser = etree.XMLParser(resolve_entities=False) - return etree.fromstring(data, parser=etree_xml_parser) - - def _default_instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: - parser = MyCustomParser() - bpmn_file_etree = self._get_etree_from_bpmn_file(bpmn_file) - parser.add_bpmn_xml(bpmn_file_etree, filename=os.path.basename(bpmn_file)) - all_related = self._find_related_bpmn_files(bpmn_file) - for related_file in all_related: - related_file_etree = self._get_etree_from_bpmn_file(related_file) - parser.add_bpmn_xml(related_file_etree, filename=os.path.basename(related_file)) - sub_parsers = list(parser.process_parsers.values()) - executable_process = None - for sub_parser in sub_parsers: - if sub_parser.process_executable: - executable_process = sub_parser.bpmn_id - if executable_process is None: - raise BpmnFileMissingExecutableProcessError( - f"Executable process cannot be found in {bpmn_file}. Test cannot run." - ) - bpmn_process_spec = parser.get_spec(executable_process) - bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) - return bpmn_process_instance + return self.process_model_test_runner_delegate.instantiate_executer(bpmn_file) def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) @@ -382,8 +402,29 @@ class ProcessModelTestRunner: ) self.test_case_results.append(test_result) + def _discover_process_model_test_cases( + self, + ) -> dict[str, str]: + test_mappings = {} + json_test_file_glob = os.path.join(self.process_model_directory_for_test_discovery, "**", "test_*.json") -class BpmnFileMissingExecutableProcessError(Exception): + for file in glob.glob(json_test_file_glob, recursive=True): + file_norm = os.path.normpath(file) + file_dir = os.path.dirname(file_norm) + json_file_name = os.path.basename(file_norm) + if self.test_case_file is None or json_file_name == self.test_case_file: + bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) + bpmn_file_path = os.path.join(file_dir, bpmn_file_name) + if os.path.isfile(bpmn_file_path): + test_mappings[file_norm] = bpmn_file_path + else: + raise MissingBpmnFileForTestCaseError( + f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" + ) + return test_mappings + + +class ProcessModeltTestRunnerBackendDelegate(ProcessModelTestRunnerMostlyPureSpiffDelegate): pass @@ -398,9 +439,7 @@ class ProcessModelTestRunnerService: process_model_directory_path, test_case_file=test_case_file, test_case_identifier=test_case_identifier, - # instantiate_executer_callback=self._instantiate_executer_callback, - # execute_task_callback=self._execute_task_callback, - # get_next_task_callback=self._get_next_task_callback, + process_model_test_runner_delegate_class=ProcessModeltTestRunnerBackendDelegate, ) def run(self) -> None: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index 78e6b2e1..2984568d 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -8,6 +8,7 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner +from spiffworkflow_backend.services.process_model_test_runner_service import UnsupporterRunnerDelegateGiven class TestProcessModelTestRunner(BaseTest): @@ -29,6 +30,16 @@ class TestProcessModelTestRunner(BaseTest): process_model_test_runner.run() assert process_model_test_runner.all_test_cases_passed(), process_model_test_runner.test_case_results + def test_will_raise_if_bad_delegate_is_given( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + ) -> None: + with pytest.raises(UnsupporterRunnerDelegateGiven): + ProcessModelTestRunner( + os.path.join(self.root_path(), "DNE"), process_model_test_runner_delegate_class=NoTestCasesFoundError + ) + def test_can_test_multiple_process_models_with_all_passing_tests( self, app: Flask,