cr-connect-workflow/crc/__init__.py
Dan 483d7e858b Improved Errors - Pick up on the new task_trace information in WorkflowException and pass it on through the api.
Also:  All script tasks should raise WorkflowTaskExecExceptions - NOT APIExceptions - this is because our scripts are executed by Spiff (not the other way around)  so the errors need to pass fluidly through spiff, and come back to use THEN we can convert them to APIErrors.  Otherwise we lose all kinds of good information about the error.
2022-03-14 16:00:53 -04:00

172 lines
6.3 KiB
Python

import logging.config
import os
import traceback
import click
import sentry_sdk
import connexion
from SpiffWorkflow import WorkflowException
from SpiffWorkflow.exceptions import WorkflowTaskExecException
from connexion import ProblemException
from flask import Response
from flask_cors import CORS
from flask_marshmallow import Marshmallow
from flask_mail import Mail
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from sentry_sdk.integrations.flask import FlaskIntegration
from apscheduler.schedulers.background import BackgroundScheduler
from werkzeug.middleware.proxy_fix import ProxyFix
connexion_app = connexion.FlaskApp(__name__)
app = connexion_app.app
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1) # respect the X-Forwarded-Proto if behind a proxy.
app.config.from_object('config.default')
if "TESTING" in os.environ and os.environ["TESTING"] == "true":
app.config.from_object('config.testing')
app.config.from_pyfile('../config/testing.py')
import logging
logging.basicConfig(level=logging.INFO)
else:
app.config.root_path = app.instance_path
app.config.from_pyfile('config.py', silent=True)
from config.logging import logging_config
logging.config.dictConfig(logging_config)
db = SQLAlchemy(app)
""":type: sqlalchemy.orm.SQLAlchemy"""
session = db.session
""":type: sqlalchemy.orm.Session"""
scheduler = BackgroundScheduler()
# Mail settings
mail = Mail(app)
migrate = Migrate(app, db)
ma = Marshmallow(app)
from crc import models
from crc import api
from crc.api import admin
from crc.services.workflow_service import WorkflowService
connexion_app.add_api('api.yml', base_path='/v1.0')
# needed function to avoid circular import
def process_waiting_tasks():
with app.app_context():
WorkflowService.do_waiting()
@app.before_first_request
def init_scheduler():
if app.config['PROCESS_WAITING_TASKS']:
scheduler.add_job(process_waiting_tasks, 'interval', minutes=1)
scheduler.add_job(WorkflowService().process_erroring_workflows, 'interval', minutes=1440)
scheduler.start()
# Convert list of allowed origins to list of regexes
origins_re = [r"^https?:\/\/%s(.*)" % o.replace('.', '\.') for o in app.config['CORS_ALLOW_ORIGINS']]
cors = CORS(connexion_app.app, origins=origins_re)
# Sentry error handling
if app.config['SENTRY_ENVIRONMENT']:
sentry_sdk.init(
environment=app.config['SENTRY_ENVIRONMENT'],
dsn="https://25342ca4e2d443c6a5c49707d68e9f40@o401361.ingest.sentry.io/5260915",
integrations=[FlaskIntegration()]
)
# Connexion Error handling
def render_errors(exception):
from crc.api.common import ApiError, ApiErrorSchema
error = ApiError(code=exception.title, message=exception.detail, status_code=exception.status)
return Response(ApiErrorSchema().dumps(error), status=500, mimetype="text/json")
connexion_app.add_error_handler(ProblemException, render_errors)
print('=== USING THESE CONFIG SETTINGS: ===')
print('APPLICATION_ROOT = ', app.config['APPLICATION_ROOT'])
print('CORS_ALLOW_ORIGINS = ', app.config['CORS_ALLOW_ORIGINS'])
print('DB_HOST = ', app.config['DB_HOST'])
print('LDAP_URL = ', app.config['LDAP_URL'])
print('PB_BASE_URL = ', app.config['PB_BASE_URL'])
print('PB_ENABLED = ', app.config['PB_ENABLED'])
print('PRODUCTION = ', app.config['PRODUCTION'])
print('TESTING = ', app.config['TESTING'])
print('TEST_UID = ', app.config['TEST_UID'])
print('ADMIN_UIDS = ', app.config['ADMIN_UIDS'])
@app.cli.command()
def clear_db():
"""Load example data into the database."""
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
@app.cli.command()
@click.argument("study_id")
@click.argument("category", required=False)
@click.argument("spec_id", required=False)
def validate_all(study_id, category=None, spec_id=None):
"""Step through all the local workflows and validate them, returning any errors. This make take forever.
Please provide a real study id to use for validation, an optional category can be specified to only validate
that category, and you can further specify a specific spec, if needed."""
from crc.services.workflow_service import WorkflowService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_spec_service import WorkflowSpecService
from crc.api.common import ApiError
from crc.models.study import StudyModel
from crc.models.user import UserModel
from flask import g
logging.root.removeHandler(logging.root.handlers[0])
study = session.query(StudyModel).filter(StudyModel.id == study_id).first()
g.user = session.query(UserModel).filter(UserModel.uid == study.user_uid).first()
g.token = "anything_is_fine_just_need_something."
specs = WorkflowSpecService().get_specs()
statuses = WorkflowProcessor.run_master_spec(WorkflowSpecService().master_spec, study)
for spec in specs:
if spec_id and spec_id != spec.id:
continue
if category and (not spec.category or spec.category.display_name != category):
continue
print("-----------------------------------------")
print(f"{spec.category.display_name} / {spec.id}")
print("-----------------------------------------")
if spec.id in statuses and statuses[spec.id]['status'] == 'disabled':
print(f"Skipping {spec.id} in category {spec.category.display_name}, it is disabled for this study.")
continue
try:
WorkflowService.test_spec(spec.id, validate_study_id=study_id)
print('Success!')
except ApiError as e:
if e.code == 'disabled_workflow':
print(f"Skipping {spec.id} in category {spec.category.display_name}, it is disabled for this study.")
else:
print(f"API Error {e.code}, validate workflow {spec.id} in Category {spec.category.display_name}. {e.message}")
continue
except WorkflowTaskExecException as e:
print(f"Workflow Error, {e}, in Task {e.task.name} validate workflow {spec.id} in Category {spec.category.display_name}")
continue
except Exception as e:
print(f"Unexpected Error ({e.__class__.__name__}), {e} validate workflow {spec.id} in Category {spec.category.display_name}")
# printing stack trace
traceback.print_exc()
print(e)
return