switch to spiff_properties branch of spiff, remove api blueprint, camunda to spiff parser/serializer

This commit is contained in:
burnettk 2022-07-13 22:52:00 -04:00
parent 3d19d7c51b
commit 100f6454f8
8 changed files with 26 additions and 308 deletions

21
poetry.lock generated
View File

@ -618,23 +618,18 @@ develop = false
[package.dependencies] [package.dependencies]
click = "^8.0.1" click = "^8.0.1"
flask = "*" flask = "*"
flask-admin = "*"
flask-bcrypt = "*"
flask-cors = "*"
flask-mail = "*"
flask-marshmallow = "*" flask-marshmallow = "*"
flask-migrate = "*" flask-migrate = "*"
flask-restful = "*" sentry-sdk = "1.7.1"
sentry-sdk = "1.7.0"
sphinx-autoapi = "^1.8.4" sphinx-autoapi = "^1.8.4"
spiffworkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "feature/parse_spiffworkflow_extensions"} spiffworkflow = "*"
werkzeug = "*" werkzeug = "*"
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/sartography/flask-bpmn" url = "https://github.com/sartography/flask-bpmn"
reference = "main" reference = "feature/with-spiff-properties"
resolved_reference = "c454e729c634c7c86ff30cf4d388480647d18d7b" resolved_reference = "c7497aabb039420d9e7c68a50d7a4b3e74100e81"
[[package]] [[package]]
name = "flask-cors" name = "flask-cors"
@ -1561,7 +1556,7 @@ requests = "*"
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "1.7.0" version = "1.7.1"
description = "Python client for Sentry (https://sentry.io)" description = "Python client for Sentry (https://sentry.io)"
category = "main" category = "main"
optional = false optional = false
@ -1806,8 +1801,8 @@ pytz = "*"
[package.source] [package.source]
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "feature/parse_spiffworkflow_extensions" reference = "feature/spiff_properties"
resolved_reference = "67054883d4040d6755bf0555f072ff85aa42093c" resolved_reference = "e108aa12da008bdd8d0319e182d28fbd3afb4c67"
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
@ -2098,7 +2093,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "c20a647c5a3e12bb5e1e9280316566acaa548e6722b7e4e13b610f25877bd4d0" content-hash = "83f09337ac9218c85a6095a8dcf82eff5153dda76453f77ca97dadfb9ed58b8a"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [

View File

@ -27,13 +27,13 @@ flask-marshmallow = "*"
flask-migrate = "*" flask-migrate = "*"
flask-restful = "*" flask-restful = "*"
werkzeug = "*" werkzeug = "*"
spiffworkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "feature/parse_spiffworkflow_extensions"} spiffworkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "feature/spiff_properties"}
# spiffworkflow = {develop = true, path = "/home/jason/projects/github/sartography/SpiffWorkflow"} # spiffworkflow = {develop = true, path = "/Users/kevin/projects/github/sartography/SpiffWorkflow"}
sentry-sdk = "1.7.0" sentry-sdk = "1.7.1"
sphinx-autoapi = "^1.8.4" sphinx-autoapi = "^1.8.4"
# flask-bpmn = {develop = true, path = "/home/jason/projects/github/sartography/flask-bpmn"} # flask-bpmn = {develop = true, path = "/home/jason/projects/github/sartography/flask-bpmn"}
# flask-bpmn = {develop = true, path = "/Users/kevin/projects/github/sartography/flask-bpmn"} # flask-bpmn = {develop = true, path = "/Users/kevin/projects/github/sartography/flask-bpmn"}
flask-bpmn = {git = "https://github.com/sartography/flask-bpmn", rev = "main"} flask-bpmn = {git = "https://github.com/sartography/flask-bpmn", rev = "feature/with-spiff-properties"}
mysql-connector-python = "^8.0.29" mysql-connector-python = "^8.0.29"
pytest-flask = "^1.2.0" pytest-flask = "^1.2.0"
pytest-flask-sqlalchemy = "^1.1.0" pytest-flask-sqlalchemy = "^1.1.0"

View File

@ -14,7 +14,6 @@ from flask_mail import Mail # type: ignore
import spiffworkflow_backend.load_database_models # noqa: F401 import spiffworkflow_backend.load_database_models # noqa: F401
from spiffworkflow_backend.config import setup_config from spiffworkflow_backend.config import setup_config
from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint
from spiffworkflow_backend.routes.api_blueprint import api_blueprint
from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint
from spiffworkflow_backend.routes.user_blueprint import user_blueprint from spiffworkflow_backend.routes.user_blueprint import user_blueprint
@ -55,7 +54,6 @@ def create_app() -> flask.app.Flask:
migrate.init_app(app, db) migrate.init_app(app, db)
app.register_blueprint(user_blueprint) app.register_blueprint(user_blueprint)
app.register_blueprint(api_blueprint)
app.register_blueprint(process_api_blueprint) app.register_blueprint(process_api_blueprint)
app.register_blueprint(api_error_blueprint) app.register_blueprint(api_error_blueprint)
app.register_blueprint(admin_blueprint, url_prefix="/admin") app.register_blueprint(admin_blueprint, url_prefix="/admin")

View File

@ -165,7 +165,6 @@ class ProcessInstanceApi:
next_task: Task | None, next_task: Task | None,
process_model_identifier: str, process_model_identifier: str,
process_group_identifier: str, process_group_identifier: str,
total_tasks: int,
completed_tasks: int, completed_tasks: int,
updated_at_in_seconds: int, updated_at_in_seconds: int,
is_review: bool, is_review: bool,
@ -178,7 +177,6 @@ class ProcessInstanceApi:
# self.navigation = navigation fixme: would be a hotness. # self.navigation = navigation fixme: would be a hotness.
self.process_model_identifier = process_model_identifier self.process_model_identifier = process_model_identifier
self.process_group_identifier = process_group_identifier self.process_group_identifier = process_group_identifier
self.total_tasks = total_tasks
self.completed_tasks = completed_tasks self.completed_tasks = completed_tasks
self.updated_at_in_seconds = updated_at_in_seconds self.updated_at_in_seconds = updated_at_in_seconds
self.title = title self.title = title
@ -199,7 +197,6 @@ class ProcessInstanceApiSchema(Schema):
"navigation", "navigation",
"process_model_identifier", "process_model_identifier",
"process_group_identifier", "process_group_identifier",
"total_tasks",
"completed_tasks", "completed_tasks",
"updated_at_in_seconds", "updated_at_in_seconds",
"is_review", "is_review",
@ -228,7 +225,6 @@ class ProcessInstanceApiSchema(Schema):
"navigation", "navigation",
"process_model_identifier", "process_model_identifier",
"process_group_identifier", "process_group_identifier",
"total_tasks",
"completed_tasks", "completed_tasks",
"updated_at_in_seconds", "updated_at_in_seconds",
"is_review", "is_review",
@ -251,7 +247,6 @@ class ProcessInstanceMetadata:
spec_version: str | None = None spec_version: str | None = None
state: str | None = None state: str | None = None
status: str | None = None status: str | None = None
total_tasks: int | None = None
completed_tasks: int | None = None completed_tasks: int | None = None
is_review: bool | None = None is_review: bool | None = None
state_message: str | None = None state_message: str | None = None
@ -270,7 +265,6 @@ class ProcessInstanceMetadata:
process_group_id=process_model.process_group_id, process_group_id=process_model.process_group_id,
state_message=process_instance.state_message, state_message=process_instance.state_message,
status=process_instance.status, status=process_instance.status,
total_tasks=process_instance.total_tasks,
completed_tasks=process_instance.completed_tasks, completed_tasks=process_instance.completed_tasks,
is_review=process_model.is_review, is_review=process_model.is_review,
process_model_identifier=process_instance.process_model_identifier, process_model_identifier=process_instance.process_model_identifier,
@ -292,7 +286,6 @@ class ProcessInstanceMetadataSchema(Schema):
"display_name", "display_name",
"description", "description",
"state", "state",
"total_tasks",
"completed_tasks", "completed_tasks",
"process_group_id", "process_group_id",
"is_review", "is_review",

View File

@ -1,59 +0,0 @@
"""Api."""
import json
import os
from flask import Blueprint
from flask import current_app
from flask import request
from flask import Response
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore
from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.spiff_workflow_connector import parse
from spiffworkflow_backend.spiff_workflow_connector import run
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(
[UserTaskConverter, BusinessRuleTaskConverter]
)
serializer = BpmnWorkflowSerializer(wf_spec_converter)
api_blueprint = Blueprint("api", __name__)
@api_blueprint.route("/run_process", methods=["POST"])
def run_process() -> Response:
"""Run_process."""
content = request.json
if content is None:
return Response(
json.dumps({"error": "Could not find json request"}),
status=400,
mimetype="application/json",
)
bpmn_spec_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
process = "order_product"
dmn = [
os.path.join(bpmn_spec_dir, "product_prices.dmn"),
os.path.join(bpmn_spec_dir, "shipping_costs.dmn"),
]
bpmn = [
os.path.join(bpmn_spec_dir, "multiinstance.bpmn"),
os.path.join(bpmn_spec_dir, "call_activity_multi.bpmn"),
]
workflow = None
process_instance = ProcessInstanceModel.query.filter().first()
if process_instance is None:
workflow = parse(process, bpmn, dmn)
else:
workflow = serializer.deserialize_json(process_instance.bpmn_json)
response = run(workflow, content.get("task_identifier"), content.get("answer"))
return Response(
json.dumps({"response": response}), status=200, mimetype="application/json"
)

View File

@ -14,6 +14,7 @@ from lxml import etree # type: ignore
from SpiffWorkflow import Task as SpiffTask # type: ignore from SpiffWorkflow import Task as SpiffTask # type: ignore
from SpiffWorkflow import TaskState from SpiffWorkflow import TaskState
from SpiffWorkflow import WorkflowException from SpiffWorkflow import WorkflowException
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskExecException # type: ignore
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore
from SpiffWorkflow.bpmn.PythonScriptEngine import Box # type: ignore from SpiffWorkflow.bpmn.PythonScriptEngine import Box # type: ignore
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
@ -23,13 +24,12 @@ from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec # type: ig
from SpiffWorkflow.bpmn.specs.events import CancelEventDefinition # type: ignore from SpiffWorkflow.bpmn.specs.events import CancelEventDefinition # type: ignore
from SpiffWorkflow.bpmn.specs.events import EndEvent from SpiffWorkflow.bpmn.specs.events import EndEvent
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser # type: ignore
from SpiffWorkflow.camunda.serializer import UserTaskConverter # type: ignore
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
from SpiffWorkflow.dmn.serializer import BusinessRuleTaskConverter # type: ignore from SpiffWorkflow.dmn.serializer import BusinessRuleTaskConverter # type: ignore
from SpiffWorkflow.exceptions import WorkflowTaskExecException # type: ignore
from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore
from SpiffWorkflow.specs import WorkflowSpec # type: ignore from SpiffWorkflow.specs import WorkflowSpec # type: ignore
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore
from SpiffWorkflow.spiff.serializer import UserTaskConverter # type: ignore
from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
from spiffworkflow_backend.models.active_task import ActiveTaskModel from spiffworkflow_backend.models.active_task import ActiveTaskModel
@ -90,10 +90,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
class MyCustomParser(BpmnDmnParser): # type: ignore class MyCustomParser(BpmnDmnParser): # type: ignore
"""A BPMN and DMN parser that can also parse Camunda forms.""" """A BPMN and DMN parser that can also parse spiffworkflow-specific extensions."""
OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES
OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES) OVERRIDE_PARSER_CLASSES.update(SpiffBpmnParser.OVERRIDE_PARSER_CLASSES)
class ProcessInstanceProcessor: class ProcessInstanceProcessor:
@ -355,8 +355,13 @@ class ProcessInstanceProcessor:
extensions = ready_or_waiting_task.task_spec.extensions extensions = ready_or_waiting_task.task_spec.extensions
form_file_name = None form_file_name = None
if "formKey" in extensions: if "properties" in extensions:
form_file_name = extensions["formKey"] properties = extensions["properties"]
if "formJsonSchemaFilename" in properties:
form_file_name = properties["formJsonSchemaFilename"]
# FIXME:
# if "formUiSchemaFilename" in properties:
# form_file_name = properties["formUiSchemaFilename"]
active_task = ActiveTaskModel( active_task = ActiveTaskModel(
spiffworkflow_task_id=str(ready_or_waiting_task.id), spiffworkflow_task_id=str(ready_or_waiting_task.id),

View File

@ -67,7 +67,7 @@ class ProcessInstanceService:
If requested, and possible, next_task is set to the current_task. If requested, and possible, next_task is set to the current_task.
""" """
navigation = processor.bpmn_process_instance.get_deep_nav_list() # navigation = processor.bpmn_process_instance.get_deep_nav_list()
# ProcessInstanceService.update_navigation(navigation, processor) # ProcessInstanceService.update_navigation(navigation, processor)
process_model_service = ProcessModelService() process_model_service = ProcessModelService()
process_model = process_model_service.get_process_model( process_model = process_model_service.get_process_model(
@ -82,7 +82,7 @@ class ProcessInstanceService:
# navigation=navigation, # navigation=navigation,
process_model_identifier=processor.process_model_identifier, process_model_identifier=processor.process_model_identifier,
process_group_identifier=processor.process_group_identifier, process_group_identifier=processor.process_group_identifier,
total_tasks=len(navigation), # total_tasks=len(navigation),
completed_tasks=processor.process_instance_model.completed_tasks, completed_tasks=processor.process_instance_model.completed_tasks,
updated_at_in_seconds=processor.process_instance_model.updated_at_in_seconds, updated_at_in_seconds=processor.process_instance_model.updated_at_in_seconds,
is_review=is_review_value, is_review=is_review_value,

View File

@ -1,214 +0,0 @@
"""Spiff Workflow Connector."""
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Union
from flask_bpmn.models.db import db
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
from SpiffWorkflow.bpmn.specs.events.event_types import CatchingEvent # type: ignore
from SpiffWorkflow.bpmn.specs.events.event_types import ThrowingEvent
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask # type: ignore
from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask # type: ignore
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore
from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser # type: ignore
from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore
from SpiffWorkflow.camunda.specs.UserTask import EnumFormField # type: ignore
from SpiffWorkflow.camunda.specs.UserTask import UserTask
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore
from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
from SpiffWorkflow.task import Task # type: ignore
from SpiffWorkflow.task import TaskState
from typing_extensions import TypedDict
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.user import UserModel
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(
[UserTaskConverter, BusinessRuleTaskConverter]
)
serializer = BpmnWorkflowSerializer(wf_spec_converter)
class Parser(BpmnDmnParser): # type: ignore
"""Parser."""
OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES
OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES)
class ProcessStatus(TypedDict, total=False):
"""ProcessStatus."""
last_task: str
upcoming_tasks: List[str]
next_activity: Dict[str, str]
def parse(process: str, bpmn_files: List[str], dmn_files: List[str]) -> BpmnWorkflow:
"""Parse."""
parser = Parser()
parser.add_bpmn_files(bpmn_files)
if dmn_files:
parser.add_dmn_files(dmn_files)
return BpmnWorkflow(parser.get_spec(process))
def format_task(task: Task, include_state: bool = True) -> str:
"""Format_task."""
if hasattr(task.task_spec, "lane") and task.task_spec.lane is not None:
lane = f"[{task.task_spec.lane}]"
else:
lane = ""
state = f"[{task.get_state_name()}]" if include_state else ""
return f"{lane} {task.task_spec.description} ({task.task_spec.name}) {state}"
def process_field(
field: Any, answer: Union[dict, None], required_user_input_fields: Dict[str, str]
) -> Union[str, int, None]:
"""Handles the complexities of figuring out what to do about each necessary user field."""
response = None
if isinstance(field, EnumFormField):
option_map = {opt.name: opt.id for opt in field.options}
options = "(" + ", ".join(option_map) + ")"
if answer is None:
required_user_input_fields[field.label] = options
else:
response = option_map[answer[field.label]]
elif field.type == "string":
if answer is None:
required_user_input_fields[field.label] = "STRING"
else:
response = answer[field.label]
else:
if answer is None:
required_user_input_fields[field.label] = "(1..)"
else:
if field.type == "long":
response = int(answer[field.label])
return response
def complete_user_task(
task: Task, answer: Optional[Dict[str, str]] = None
) -> Dict[Any, Any]:
"""Complete_user_task."""
if task.data is None:
task.data = {}
required_user_input_fields: Dict[str, str] = {}
for field in task.task_spec.form.fields:
response = process_field(field, answer, required_user_input_fields)
if answer:
task.update_data_var(field.id, response)
return required_user_input_fields
def get_state(workflow: BpmnWorkflow) -> ProcessStatus:
"""Print_state."""
task = workflow.last_task
return_json: ProcessStatus = {"last_task": format_task(task), "upcoming_tasks": []}
display_types = (UserTask, ManualTask, ScriptTask, ThrowingEvent, CatchingEvent)
all_tasks = [
task
for task in workflow.get_tasks()
if isinstance(task.task_spec, display_types)
]
upcoming_tasks = [
task for task in all_tasks if task.state in [TaskState.READY, TaskState.WAITING]
]
for _idx, task in enumerate(upcoming_tasks):
return_json["upcoming_tasks"].append(format_task(task))
return return_json
def create_user() -> UserModel:
"""Create_user."""
user = UserModel(username="user1")
db.session.add(user)
db.session.commit()
return user
def create_process_instance() -> ProcessInstanceModel:
"""Create_process_instance."""
user = UserModel.query.filter().first()
if user is None:
user = create_user()
process_instance = ProcessInstanceModel(
process_model_identifier="process_model1", process_initiator_id=user.id
)
db.session.add(process_instance)
db.session.commit()
return process_instance
def run(
workflow: BpmnWorkflow,
task_identifier: Optional[str] = None,
answer: Optional[Dict[str, str]] = None,
) -> Union[ProcessStatus, Dict[str, str]]:
"""Run."""
workflow.do_engine_steps()
tasks_status = ProcessStatus()
if workflow.is_completed():
return tasks_status
ready_tasks = workflow.get_ready_user_tasks()
options = {}
formatted_options = {}
for idx, task in enumerate(ready_tasks):
option = format_task(task, False)
options[str(idx + 1)] = task
formatted_options[str(idx + 1)] = option
if task_identifier is None:
return formatted_options
next_task = options[task_identifier]
if isinstance(next_task.task_spec, UserTask):
if answer is None:
return complete_user_task(next_task)
else:
complete_user_task(next_task, answer)
next_task.complete()
elif isinstance(next_task.task_spec, ManualTask):
next_task.complete()
else:
next_task.complete()
workflow.refresh_waiting_tasks()
workflow.do_engine_steps()
tasks_status = get_state(workflow)
ready_tasks = workflow.get_ready_user_tasks()
formatted_options = {}
for idx, task in enumerate(ready_tasks):
option = format_task(task, False)
formatted_options[str(idx + 1)] = option
state = serializer.serialize_json(workflow)
process_instance = ProcessInstanceModel.query.filter().first()
if process_instance is None:
process_instance = create_process_instance()
process_instance.bpmn_json = state
db.session.add(process_instance)
db.session.commit()
tasks_status["next_activity"] = formatted_options
return tasks_status