scalable-pypeline 1.1.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,245 @@
1
+ """ Pipeline APIs
2
+ """
3
+ import logging
4
+
5
+ from celery.canvas import _chain
6
+ from celery_dyrygent.workflows import Workflow
7
+ from flask import jsonify, request
8
+ from flask.views import MethodView
9
+ from rho_web.smorest import Blueprint
10
+ from rho_web.response import abort
11
+ from marshmallow import Schema, fields
12
+ from marshmallow.exceptions import ValidationError
13
+ from pypeline.constants import API_DOC_RESPONSES, API_DOC_PARAMS, API_PATH_V1
14
+ from pypeline.flask.decorators import require_accesskey
15
+ from pypeline.flask.api.utils import chain_helper
16
+ from pypeline.utils.task_utils import PipelineResult
17
+ from pypeline.utils.config_utils import retrieve_latest_pipeline_config
18
+ from pypeline.pipeline_config_schema import BasePipelineSchema, PipelineSchemaV1
19
+
20
+ logger = logging.getLogger(__name__)
21
+ bp = Blueprint('pipelines', __name__, url_prefix=API_PATH_V1 + '/pipelines')
22
+
23
+
24
+ class InvokePipelineSchema(Schema):
25
+ """ Incoming schema for invoking a pipeline
26
+ """
27
+ chain_payload = fields.Raw(
28
+ description='Payload contains whatever arguments the pipeline expects '
29
+ 'to be passed to each node in the graph.',
30
+ example={
31
+ 'document_id': '123',
32
+ 'send_alert': True
33
+ },
34
+ required=False)
35
+
36
+
37
+ class InvokePipelineResponseSchema(Schema):
38
+ execution_id = fields.String()
39
+ pipeline_id = fields.String()
40
+ status = fields.String()
41
+
42
+
43
+ class GetPipelineResultResponseSchema(Schema):
44
+ execution_id = fields.String()
45
+ result = fields.Raw()
46
+ result_ttl = fields.Integer()
47
+ results = fields.Raw()
48
+ status = fields.String()
49
+ status_message = fields.String()
50
+
51
+
52
+ @bp.route('/')
53
+ class Pipelines(MethodView):
54
+ """ Operations against all pipelines.
55
+ """
56
+ @require_accesskey
57
+ @bp.doc(responses=API_DOC_RESPONSES,
58
+ parameters=[API_DOC_PARAMS['accesskey']],
59
+ tags=['Pipelines'])
60
+ def get(self):
61
+ """ Retrieve list of available pipelines.
62
+ """
63
+ access_key = request.headers.get('accesskey')
64
+ pipeline_config_api_resp = retrieve_latest_pipeline_config(
65
+ access_key=access_key)
66
+
67
+ if pipeline_config_api_resp is None:
68
+ abort(404)
69
+
70
+ try:
71
+ pipelines = []
72
+ for p in pipeline_config_api_resp:
73
+ PipelineSchema = \
74
+ BasePipelineSchema.get_by_version(p['schemaVersion'])
75
+ pipeline_config = PipelineSchema().load(p)
76
+ pipelines.append(pipeline_config)
77
+ except ValidationError as e:
78
+ msg = f"Invalid pipeline configuration: {e}"
79
+ return jsonify({'message': msg}), 202
80
+
81
+ return jsonify(pipelines)
82
+
83
+
84
+ @bp.route('/<string:pipeline_id>')
85
+ class PipelineInfo(MethodView):
86
+ """ Operations against a single pipeline
87
+ """
88
+ @require_accesskey
89
+ @bp.doc(responses=API_DOC_RESPONSES,
90
+ parameters=[
91
+ API_DOC_PARAMS['accesskey'], {
92
+ 'in': 'path',
93
+ 'name': 'pipeline_id',
94
+ 'description':
95
+ 'pipeline_id for which to retrieve metrics.',
96
+ 'type': 'string',
97
+ 'example': 'my_pipeline',
98
+ 'required': True
99
+ }
100
+ ],
101
+ tags=['Pipelines'])
102
+ def get(self, pipeline_id: str):
103
+ """ Retrieve details about a specific pipeline.
104
+ """
105
+ access_key = request.headers.get('accesskey')
106
+ pipeline_config_api_resp = retrieve_latest_pipeline_config(
107
+ pipeline_id=pipeline_id, access_key=access_key)
108
+
109
+ if pipeline_config_api_resp is None:
110
+ abort(404)
111
+
112
+ try:
113
+ pipeline_config = PipelineSchemaV1().load(pipeline_config_api_resp)
114
+ except ValidationError as e:
115
+ msg = f"Invalid pipeline configuration: {e}"
116
+ return jsonify({'message': msg}), 202
117
+
118
+ return jsonify(pipeline_config)
119
+
120
+
121
+ @bp.route('/invoke/<string:pipeline_id>')
122
+ class PipelineInvoke(MethodView):
123
+ """ Operations involed with pipeline invocation
124
+ """
125
+ @require_accesskey
126
+ @bp.doc(responses=API_DOC_RESPONSES,
127
+ parameters=[
128
+ API_DOC_PARAMS['accesskey'], {
129
+ 'in': 'path',
130
+ 'name': 'pipeline_id',
131
+ 'description':
132
+ 'pipeline_id for which to retrieve metrics.',
133
+ 'type': 'string',
134
+ 'example': 'my_pipeline',
135
+ 'required': True
136
+ }
137
+ ],
138
+ tags=['Pipelines'])
139
+ @bp.arguments(InvokePipelineSchema)
140
+ @bp.response(InvokePipelineResponseSchema)
141
+ def post(self, payload: dict, pipeline_id: str):
142
+ """ Invoke a pipeline by it's ID; optionally provide pipeline arguments.
143
+ """
144
+
145
+ access_key = request.headers.get('accesskey')
146
+ pipeline_config = retrieve_latest_pipeline_config(
147
+ pipeline_id=pipeline_id, access_key=access_key)
148
+
149
+ if pipeline_config is None:
150
+ return abort(404)
151
+
152
+ retval = {'pipeline_id': pipeline_id, 'status': ''}
153
+ try:
154
+ # TODO - ideally we can validate the payload *at this stage*
155
+ # before the chain is ever invoked so we can handle issues
156
+ # without kicking off work.
157
+ payload = payload['chain_payload']\
158
+ if 'chain_payload' in payload else {}
159
+
160
+ gen = chain_helper(pipeline_id=pipeline_id,
161
+ access_key=access_key,
162
+ chain_payload=payload)
163
+
164
+ if gen.chain is None:
165
+ abort(400, message=gen.loading_message)
166
+
167
+ chain: _chain = gen.chain
168
+ wf: Workflow = Workflow()
169
+ wf.add_celery_canvas(chain)
170
+ wf.apply_async()
171
+ retval['status'] = 'success'
172
+ retval['execution_id'] = gen.execution_id
173
+ # Initialize the cached result
174
+ pr = PipelineResult(gen.execution_id, status='pending')
175
+ pr.save()
176
+
177
+ except Exception as e:
178
+ msg = "Failed to invoke pipeline ... {}".format(pipeline_id)
179
+ logger.error(msg)
180
+ logger.exception(f"{e}")
181
+ abort(500, message=msg)
182
+
183
+ return jsonify(retval)
184
+
185
+
186
+ results_responses = API_DOC_RESPONSES.copy()
187
+ results_responses[202] = {
188
+ 'code': 202,
189
+ 'description': 'Pipeline is still running. Try again later.'
190
+ }
191
+ results_responses[204] = {
192
+ 'code': 204,
193
+ 'description': 'The execution results have expired. Re-run pipeline.'
194
+ }
195
+
196
+
197
+ @bp.route('/results/<string:execution_id>')
198
+ class PipelineResults(MethodView):
199
+ """ Operations with respect to pipeline results
200
+ """
201
+ @require_accesskey
202
+ @bp.doc(responses=results_responses,
203
+ parameters=[
204
+ API_DOC_PARAMS['accesskey'], {
205
+ 'in': 'path',
206
+ 'name': 'execution_id',
207
+ 'description':
208
+ 'execution_id for which to retrieve results',
209
+ 'type': 'string',
210
+ 'example': '4c595cca-9bf1-4150-8c34-6b43faf276c8',
211
+ 'required': True
212
+ }
213
+ ],
214
+ tags=['Pipelines'])
215
+ @bp.response(GetPipelineResultResponseSchema)
216
+ def get(self, execution_id: str):
217
+ """ Retrieve results of a pipeline's execution based on execution_id
218
+
219
+ NOTE: Cached results expire after a time window so are not available
220
+ forever.
221
+
222
+ TODO: Need to add response marshalling/schema here.
223
+ """
224
+ try:
225
+ pr = PipelineResult(execution_id)
226
+ pr.load()
227
+ retval = pr.to_dict()
228
+ if pr.status == 'unavailable':
229
+ retval['status_message'] = 'Results expired. Re-run pipeline.'
230
+ return retval, 204
231
+
232
+ if pr.status == 'pending':
233
+ retval['status_message'] = 'Results pending. Check again soon.'
234
+ return retval, 202
235
+
236
+ else:
237
+ retval['status_message'] = 'Results available.'
238
+ return retval, 200
239
+
240
+ except Exception as e:
241
+ msg = "Failed to retrieve results for execution id: {}".format(
242
+ execution_id)
243
+ logger.error(msg)
244
+ logger.exception(f"{e}")
245
+ abort(500, message=msg)
@@ -0,0 +1,67 @@
1
+ """ API Endpoints for Scheduled Tasks
2
+ """
3
+ import os
4
+ import logging
5
+ from flask import jsonify, request
6
+ from flask.views import MethodView
7
+ from marshmallow import Schema, fields
8
+ from rho_web.smorest import Blueprint
9
+ from rho_web.response import abort
10
+ from marshmallow.exceptions import ValidationError
11
+ from pypeline.constants import API_DOC_RESPONSES, API_DOC_PARAMS,\
12
+ API_PATH_V1
13
+ from pypeline.utils.config_utils import retrieve_latest_schedule_config, \
14
+ update_schedule_config
15
+ from pypeline.schedule_config_schema import BaseScheduleSchema
16
+ from pypeline.flask.decorators import require_accesskey
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ bp = Blueprint('schedules', __name__, url_prefix=API_PATH_V1 + '/schedules')
21
+
22
+
23
+ @bp.route('/')
24
+ class Schedules(MethodView):
25
+ """ Operations related to schedules
26
+ """
27
+ @require_accesskey
28
+ @bp.doc(responses=API_DOC_RESPONSES,
29
+ parameters=[API_DOC_PARAMS['accesskey']],
30
+ tags=['Schedules'])
31
+ def get(self):
32
+ """ Retrieve list of available schedule entries.
33
+ """
34
+ access_key = request.headers.get('accesskey')
35
+ try:
36
+ schedule_config = retrieve_latest_schedule_config(
37
+ access_key=access_key)
38
+ except ValidationError:
39
+ abort(400, message="Invalid schedule found ...")
40
+
41
+ if schedule_config is None:
42
+ abort(404)
43
+
44
+ return jsonify(schedule_config)
45
+
46
+ @require_accesskey
47
+ @bp.doc(responses=API_DOC_RESPONSES,
48
+ parameters=[API_DOC_PARAMS['accesskey']],
49
+ tags=['Schedules'])
50
+ @bp.arguments(BaseScheduleSchema)
51
+ def post(self, payload: dict):
52
+ """ Update a deployment's schedules. Primarily used to update dynamic
53
+ keys such as last run at and total run count. This does not allow
54
+ overloading schedules, only updating select keys on known schedule
55
+ entries (as in, this is not destructive).
56
+ """
57
+ access_key = request.headers.get('accesskey')
58
+ try:
59
+ success = update_schedule_config(new_schedule_config=payload,
60
+ access_key=access_key)
61
+ except ValidationError as e:
62
+ abort(400, message=e)
63
+
64
+ if not success:
65
+ abort(500)
66
+
67
+ return jsonify({'message': 'Schedule update successful ...'})
@@ -0,0 +1,36 @@
1
+ """ Utilities for Sermos APIs and interacting with Pipelines/Schedules
2
+ """
3
+ import logging
4
+ from typing import Union
5
+ from pypeline.utils.task_utils import PipelineGenerator
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def chain_helper(pipeline_id: str,
11
+ access_key: Union[str, None] = None,
12
+ chain_payload: Union[dict, None] = None,
13
+ add_retry: bool = True,
14
+ queue: Union[str, None] = None,
15
+ default_task_ttl: int = None):
16
+ """ Helper method to generate a pipeline chain *with* error handling.
17
+
18
+ Usage:
19
+ my_chain = chain_helper('pipeline-name')
20
+ my_chain.delay()
21
+ """
22
+ # Get our pipeline. The PipelineGenerator will use the PipelineRunWrapper
23
+ # to cache this "run" of the pipeline.
24
+ gen = PipelineGenerator(pipeline_id,
25
+ access_key=access_key,
26
+ queue=queue,
27
+ default_task_ttl=default_task_ttl,
28
+ add_retry=add_retry,
29
+ chain_payload=chain_payload)
30
+ if gen.good_to_go:
31
+ # Generate our 'chain', which is the grouping of celery constructs that
32
+ # allows our dag to run asynchronously and synchronously according to
33
+ # the adjacency list defined in our pipeline configuration.
34
+ gen.generate_chain()
35
+
36
+ return gen
@@ -0,0 +1,92 @@
1
+ """ Flask specific decorators (primarily for auth activities and app context)
2
+ """
3
+ import os
4
+ import logging
5
+ from http import HTTPStatus
6
+ from functools import wraps
7
+ import requests
8
+ from rhodb.redis_conf import RedisConnector
9
+ from flask import current_app, request
10
+ from rho_web.response import abort
11
+ from pypeline.flask import oidc
12
+ from pypeline.constants import DEFAULT_AUTH_URL, AUTH_LOCK_KEY, \
13
+ AUTH_LOCK_DURATION, USING_SERMOS_CLOUD
14
+
15
+ logger = logging.getLogger(__name__)
16
+ redis_conn = RedisConnector().get_connection()
17
+
18
+
19
+ def require_login(fn):
20
+ """ Utilize Flask RhoAuth to secure a route with @require_login decorator
21
+ """
22
+ secure_fn = oidc.require_login(fn)
23
+
24
+ @wraps(fn)
25
+ def decorated(*args, **kwargs):
26
+ if str(current_app.config['RHOAUTH_ENABLED']).lower() == 'true':
27
+ return secure_fn(*args, **kwargs)
28
+ return fn(*args, **kwargs)
29
+
30
+ return decorated
31
+
32
+
33
+ def validate_access_key(access_key: str = None):
34
+ """ Verify whether an Access Key is valid according to Sermos Cloud.
35
+
36
+ If deploying in 'local' mode, no validation is done. To deploy in local
37
+ mode, set DEFAULT_BASE_URL=local in your environment.
38
+ """
39
+ # Always 'valid' in local mode
40
+ if not USING_SERMOS_CLOUD:
41
+ return True
42
+
43
+ # If get access key from either provided val or environment
44
+ # if None provided.
45
+ access_key = os.environ.get('SERMOS_ACCESS_KEY', access_key)
46
+
47
+ # Invalid if None, no need to ask.
48
+ if access_key is None:
49
+ return False
50
+
51
+ # Ask cache first
52
+ # TODO update to remove Redis as a dependency, merely an optional feature.
53
+ validated = redis_conn.get(AUTH_LOCK_KEY)
54
+ if validated is not None:
55
+ return True
56
+
57
+ # Ask Sermos Cloud (Note: Sermos Cloud's API expects `apikey`)
58
+ headers = {'apikey': access_key}
59
+ r = requests.post(DEFAULT_AUTH_URL, headers=headers, verify=True)
60
+
61
+ if r.status_code == 200:
62
+ redis_conn.setex(AUTH_LOCK_KEY, AUTH_LOCK_DURATION, '')
63
+ return True
64
+ return False
65
+
66
+
67
+ def require_accesskey(fn):
68
+ """ Convenience decorator to add to a web route (typically an API)
69
+ when using Flask.
70
+
71
+ Usage::
72
+ from sermos import Blueprint, ApiServices
73
+ bp = Blueprint('api_routes', __name__, url_prefix='/api')
74
+
75
+ @bp.route('/my-api-route')
76
+ class ApiClass(MethodView):
77
+ @require_access_key
78
+ def post(self, payload: dict):
79
+ return {}
80
+ """
81
+ @wraps(fn)
82
+ def decorated_view(*args, **kwargs):
83
+ access_key = request.headers.get('accesskey')
84
+ if not access_key:
85
+ access_key = request.args.get('accesskey')
86
+
87
+ if validate_access_key(access_key):
88
+ return fn(*args, **kwargs)
89
+
90
+ abort(HTTPStatus.UNAUTHORIZED)
91
+
92
+ return decorated_view
@@ -0,0 +1,219 @@
1
+ """ Sermos implementation as a Flask extension
2
+ """
3
+ import os
4
+ if os.getenv('USE_GEVENT', 'false').lower() == 'true':
5
+ import gevent.monkey
6
+ gevent.monkey.patch_all()
7
+
8
+ import logging
9
+ from functools import partial
10
+ from flask import Flask
11
+ from werkzeug.middleware.proxy_fix import ProxyFix
12
+ from rho_web.smorest import Api, Blueprint
13
+ from pypeline.utils.task_utils import TaskRunner, PipelineResult
14
+ from pypeline.logging_config import setup_logging
15
+ from pypeline.extensions import sermos_config
16
+ from pypeline.flask import oidc
17
+ from pypeline.constants import DEFAULT_OPENAPI_CONFIG, DEFAULT_RHOAUTH_CONFIG
18
+ from pypeline import __version__
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class FlaskSermos:
24
+ """ Sermos Flask extension.
25
+ """
26
+ def __init__(self, app: Flask = None):
27
+ """ Class init
28
+ """
29
+ self.app = app
30
+ self.sermos_config = sermos_config if sermos_config is not None else {}
31
+
32
+ if app is not None:
33
+ self.init_app(app)
34
+
35
+ def init_app(self, app: Flask, init_api: bool = False):
36
+ """ Sermos bootstrapping process.
37
+
38
+ Application config variables to set include:
39
+
40
+ SERMOS_CLIENT_VERSION (default: v?.?.?)
41
+ RHOAUTH_ENABLED (default: True)
42
+ SERMOS_HIJACK_ROOT_LOGGER (default: False)
43
+
44
+ Optional, if `init_api` is True:
45
+
46
+ API_DOCUMENTATION_TITLE
47
+ API_DOCUMENTATION_DESCRIPTION
48
+ OPENAPI_VERSION
49
+ OPENAPI_URL_PREFIX
50
+ OPENAPI_SWAGGER_APP_NAME
51
+ OPENAPI_SWAGGER_UI_PATH
52
+ OPENAPI_SWAGGER_BASE_TEMPLATE
53
+ OPENAPI_SWAGGER_URL
54
+ OPENAPI_SWAGGER_UI_URL
55
+ SWAGGER_UI_DOC_EXPANSION
56
+ EXPLAIN_TEMPLATE_LOADING
57
+
58
+ Args:
59
+ app (Flask): Flask Application to initialize.
60
+ init_api (bool): If `True`, Sermos will initialize its
61
+ core APIs (including Pipelines, Scheduled Tasks, etc.) and
62
+ provide a pre-configured OpenAPI Spec/Swagger UI interface
63
+ available at the route defined in your application's config
64
+ under `OPENAPI_URL_PREFIX` (default `/api`). Refer to
65
+ [flask-smorest](https://flask-smorest.readthedocs.io/en/latest/openapi.html)
66
+ documentation for additional configuration options.
67
+ """
68
+ # Ensure there's a SERMOS_CLIENT_VERSION on app config
69
+ app.config.setdefault(
70
+ 'SERMOS_CLIENT_VERSION',
71
+ app.config.get("SERMOS_CLIENT_VERSION", "v?.?.?"))
72
+
73
+ # Bootstrap logging if app requests
74
+ self._bootstrap_logging(app)
75
+
76
+ app.wsgi_app = ProxyFix(app.wsgi_app)
77
+ app.url_map.strict_slashes = False
78
+
79
+ # Create and register the sermos blueprint
80
+ bp = Blueprint('sermos',
81
+ __name__,
82
+ template_folder='../templates',
83
+ static_folder='../static',
84
+ url_prefix='/sermos')
85
+ app.register_blueprint(bp)
86
+
87
+ # Bootstrap RhoAuth if app requests
88
+ self._bootstrap_rhoauth(app)
89
+
90
+ # Bootstrap api if app requests
91
+ if init_api is True:
92
+ self._bootstrap_api(app)
93
+
94
+ def _bootstrap_rhoauth(self, app: Flask):
95
+ """ If RHOAUTH_ENABLED is True, bootstrap the application with default
96
+ configuration, precedent given to the provide app.
97
+ """
98
+ # TODO Remove rhoauth by default and/or entirely in future release.
99
+ rhoauth_enabled = str(app.config.get('RHOAUTH_ENABLED',
100
+ True)).lower() == 'true'
101
+ app.config.setdefault('RHOAUTH_ENABLED', rhoauth_enabled)
102
+ if rhoauth_enabled is True:
103
+ # Set useful defaults so users don't need to request.
104
+ for rhoauth_config in DEFAULT_RHOAUTH_CONFIG:
105
+ app.config.setdefault(
106
+ rhoauth_config[0],
107
+ app.config.get(rhoauth_config[0], rhoauth_config[1]))
108
+
109
+ # Initialize open id connect for authentication
110
+ if rhoauth_enabled is True:
111
+ logger.info("Initializing using RhoAuth ...")
112
+ self.oidc = oidc
113
+ self.oidc.init_app(app)
114
+
115
+ @app.route('/logout')
116
+ def logout():
117
+ return self.oidc.logout("/")
118
+
119
+ @staticmethod
120
+ def _bootstrap_logging(app: Flask):
121
+ """ If application requests Sermos logging, enable here
122
+
123
+ Main reason to do this is to have clear versioning in each log
124
+ and to supress the elasticsearch log level by default, which is
125
+ extremely verbose if using elasticsearch-py. There is no dependency
126
+ on elasticsearch - we simply create the logger and upgrade level
127
+ because it's extremely annoying if you do need to use ES.
128
+ """
129
+ if app.config.get('SERMOS_HIJACK_ROOT_LOGGER', False):
130
+ overload_es = app.config.get('OVERLOAD_ES_LOGGING', True)
131
+ logger.info("Initializing using Sermos logging ...")
132
+ setup_logging(app_version=__version__,
133
+ client_version=app.config['SERMOS_CLIENT_VERSION'],
134
+ default_level=app.config.get("LOG_LEVEL", "INFO"),
135
+ overload_elasticsearch=overload_es,
136
+ establish_logging_config=True)
137
+
138
+ def _bootstrap_api(self, app: Flask):
139
+ """ If initializing the API, we will create the core Sermos API paths
140
+ and initialize the default Swagger documentation.
141
+ """
142
+ # Set sensible defaults for Swagger docs. Provided `app` will
143
+ # take precedent.
144
+ for swagger_config in DEFAULT_OPENAPI_CONFIG:
145
+ app.config.setdefault(
146
+ swagger_config[0],
147
+ app.config.get(swagger_config[0], swagger_config[1]))
148
+
149
+ # Attempt to override with values from client's sermos.yaml if
150
+ # they are available. This will add new tags and new docs if
151
+ # defined and add to the core Sermos API docs.
152
+ api_config = self.sermos_config.get('apiConfig', {})
153
+ api_docs = api_config.get('apiDocumentation', {})
154
+
155
+ custom_tags = api_config.get('prefixDescriptions', [])
156
+
157
+ app.config['SERMOS_CLIENT_VERSION'] = \
158
+ api_docs.get('version', None) \
159
+ if api_docs.get('version', None) is not None \
160
+ else app.config['SERMOS_CLIENT_VERSION']
161
+
162
+ app.config['API_DOCUMENTATION_TITLE'] = \
163
+ api_docs.get('title', None) \
164
+ if api_docs.get('title', None) is not None \
165
+ else app.config['API_DOCUMENTATION_TITLE']
166
+
167
+ app.config['API_DOCUMENTATION_DESCRIPTION'] = \
168
+ api_docs.get('description', None) \
169
+ if api_docs.get('description', None) is not None \
170
+ else app.config['API_DOCUMENTATION_DESCRIPTION']
171
+
172
+ # Set default Sermos Tags along with custom tags from sermos.yaml
173
+ tags = [{
174
+ 'name': 'Pipelines',
175
+ 'description': 'Operations related to Pipelines'
176
+ }, {
177
+ 'name': 'Schedules',
178
+ 'description': 'Operations related to Schedules'
179
+ }] + custom_tags
180
+
181
+ # Set up the initializing spec kwargs for API
182
+ spec_kwargs = {
183
+ 'title': app.config['API_DOCUMENTATION_TITLE'],
184
+ 'version': f"Sermos: {__version__} - "
185
+ f"Client: {app.config['SERMOS_CLIENT_VERSION']}",
186
+ 'description': app.config['API_DOCUMENTATION_DESCRIPTION'],
187
+ 'tags': tags
188
+ }
189
+ try:
190
+ # Initialize the API documentation and ensure RhoAuth validates
191
+ # correct `role`
192
+ if str(app.config['RHOAUTH_ENABLED']).lower() == 'true':
193
+ decorator = partial(self.oidc.require_login,
194
+ roles=['sermos-client-api-docs'])
195
+ api = Api(auth_decorator=decorator)
196
+ else:
197
+ api = Api()
198
+
199
+ api.init_app(app, spec_kwargs=spec_kwargs)
200
+
201
+ # Register available Sermos API Namespaces
202
+ self._register_api_namespaces(api)
203
+
204
+ except Exception as e:
205
+ api = None
206
+ logging.exception(f"Unable to initialize API ... {e}")
207
+
208
+ # Register the Sermos Core API as an extension for use in Client App
209
+ app.extensions.setdefault('sermos_core_api', api)
210
+
211
+ @staticmethod
212
+ def _register_api_namespaces(api: Api):
213
+ """ Register Default API namespaces
214
+ TODO add metrics APIs
215
+ """
216
+ from pypeline.flask.api.pipelines import bp as pipelinesbp
217
+ api.register_blueprint(pipelinesbp)
218
+ from pypeline.flask.api.schedules import bp as schedulesbp
219
+ api.register_blueprint(schedulesbp)