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.
- pypeline/__init__.py +1 -0
- pypeline/celery.py +270 -0
- pypeline/celery_beat.py +254 -0
- pypeline/cli/__init__.py +0 -0
- pypeline/cli/config_server.py +48 -0
- pypeline/cli/core.py +32 -0
- pypeline/cli/deploy.py +138 -0
- pypeline/cloud.py +80 -0
- pypeline/constants.py +139 -0
- pypeline/deploy.py +167 -0
- pypeline/extensions.py +16 -0
- pypeline/flask/__init__.py +28 -0
- pypeline/flask/api/__init__.py +0 -0
- pypeline/flask/api/pipelines.py +245 -0
- pypeline/flask/api/schedules.py +67 -0
- pypeline/flask/api/utils.py +36 -0
- pypeline/flask/decorators.py +92 -0
- pypeline/flask/flask_sermos.py +219 -0
- pypeline/generators.py +196 -0
- pypeline/lib/__init__.py +0 -0
- pypeline/lib/config_server.py +159 -0
- pypeline/logging_config.py +171 -0
- pypeline/pipeline_config_schema.py +197 -0
- pypeline/schedule_config_schema.py +210 -0
- pypeline/sermos_yaml.py +737 -0
- pypeline/utils/__init__.py +0 -0
- pypeline/utils/config_utils.py +327 -0
- pypeline/utils/graph_utils.py +144 -0
- pypeline/utils/module_utils.py +119 -0
- pypeline/utils/task_utils.py +803 -0
- scalable_pypeline-1.1.0.dist-info/LICENSE +177 -0
- scalable_pypeline-1.1.0.dist-info/METADATA +166 -0
- scalable_pypeline-1.1.0.dist-info/RECORD +38 -0
- scalable_pypeline-1.1.0.dist-info/WHEEL +6 -0
- scalable_pypeline-1.1.0.dist-info/entry_points.txt +2 -0
- scalable_pypeline-1.1.0.dist-info/top_level.txt +2 -0
- tests/fixtures/__init__.py +1 -0
- tests/fixtures/s3_fixtures.py +52 -0
pypeline/cli/deploy.py
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
""" Command Line Utilities for Sermos Deployments
|
2
|
+
"""
|
3
|
+
import logging
|
4
|
+
import click
|
5
|
+
from pypeline.deploy import SermosDeploy
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
|
10
|
+
@click.group()
|
11
|
+
def deployment():
|
12
|
+
""" Deployment command group.
|
13
|
+
"""
|
14
|
+
|
15
|
+
|
16
|
+
@deployment.command()
|
17
|
+
@click.option('--pkg-name', required=False, default=None)
|
18
|
+
@click.option('--sermos-yaml', required=False, default=None)
|
19
|
+
@click.option('--output-file', required=False, default=None)
|
20
|
+
def validate(pkg_name: str = None,
|
21
|
+
sermos_yaml: str = None,
|
22
|
+
output_file: str = None):
|
23
|
+
""" Validate a compiled Sermos yaml is ready for deployment.
|
24
|
+
|
25
|
+
Arguments::
|
26
|
+
|
27
|
+
pkg-name (optional): Directory name for your Python package.
|
28
|
+
e.g. my_package_name If none provided, will check environment
|
29
|
+
for `SERMOS_CLIENT_PKG_NAME`. If not found, will exit.
|
30
|
+
|
31
|
+
sermos-yaml (optional): Path to find your `sermos.yaml`
|
32
|
+
configuration file. Defaults to `sermos.yaml`
|
33
|
+
"""
|
34
|
+
# Instantiate SermosDeploy
|
35
|
+
sd = SermosDeploy(access_key='fake',
|
36
|
+
pkg_name=pkg_name,
|
37
|
+
sermos_yaml_filename=sermos_yaml)
|
38
|
+
|
39
|
+
# Validate deployment
|
40
|
+
sd.validate_deployment(output_file=output_file)
|
41
|
+
click.echo("Configuration is Valid and ready to Deploy.")
|
42
|
+
|
43
|
+
|
44
|
+
@deployment.command()
|
45
|
+
@click.option('--deployment-id', required=False, default=None)
|
46
|
+
@click.option('--access-key', required=False, default=None)
|
47
|
+
@click.option('--pkg-name', required=False, default=None)
|
48
|
+
@click.option('--sermos-yaml', required=False, default=None)
|
49
|
+
@click.option('--commit-hash', required=False, default=None)
|
50
|
+
@click.option('--base-url', required=False, default=None)
|
51
|
+
@click.option('--deploy-branch', required=False, default='main')
|
52
|
+
def deploy(deployment_id: str = None,
|
53
|
+
access_key: str = None,
|
54
|
+
pkg_name: str = None,
|
55
|
+
sermos_yaml: str = None,
|
56
|
+
commit_hash: str = None,
|
57
|
+
base_url: str = None,
|
58
|
+
deploy_branch: str = 'main'):
|
59
|
+
""" Invoke a Sermos build for your application.
|
60
|
+
|
61
|
+
Arguments:
|
62
|
+
|
63
|
+
deployment-id (optional): UUID for Deployment. Find in your Sermos
|
64
|
+
Cloud Console. Will look under `SERMOS_DEPLOYMENT_ID` in
|
65
|
+
environment if not provided.
|
66
|
+
|
67
|
+
access-key (optional): Defaults to checking the environment for
|
68
|
+
`SERMOS_ACCESS_KEY`. If not found, will exit.
|
69
|
+
|
70
|
+
pkg-name (optional): Directory name for your Python package.
|
71
|
+
e.g. my_package_name If none provided, will check environment
|
72
|
+
for `SERMOS_CLIENT_PKG_NAME`. If not found, will exit.
|
73
|
+
|
74
|
+
sermos-yaml (optional): Path to find your `sermos.yaml`
|
75
|
+
configuration file. Defaults to `sermos.yaml`
|
76
|
+
|
77
|
+
commit-hash (optional): The specific commit hash of your git repo
|
78
|
+
to deploy. If not provided, then current HEAD as of invocation
|
79
|
+
will be used. This is the default usage, and is useful in the
|
80
|
+
case of a CI/CD pipeline such that the Sermos deployment is
|
81
|
+
invoked after your integration passes.
|
82
|
+
|
83
|
+
base-url (optional): Defaults to primary Sermos Cloud base URL.
|
84
|
+
Only modify this if there is a specific, known reason to do so.
|
85
|
+
|
86
|
+
deploy-branch (optional): Defaults to 'main'. Only modify this
|
87
|
+
if there is a specific, known reason to do so.
|
88
|
+
"""
|
89
|
+
# Instantiate SermosDeploy
|
90
|
+
sd = SermosDeploy(deployment_id=deployment_id,
|
91
|
+
access_key=access_key,
|
92
|
+
pkg_name=pkg_name,
|
93
|
+
sermos_yaml_filename=sermos_yaml,
|
94
|
+
commit_hash=commit_hash,
|
95
|
+
base_url=base_url,
|
96
|
+
deploy_branch=deploy_branch)
|
97
|
+
|
98
|
+
# Validate deployment
|
99
|
+
sd.validate_deployment()
|
100
|
+
|
101
|
+
# Invoke deployment
|
102
|
+
result = sd.invoke_deployment()
|
103
|
+
content = result.json()
|
104
|
+
if result.status_code < 300:
|
105
|
+
click.echo(content['data']['status'])
|
106
|
+
else:
|
107
|
+
logger.error(f"{content}")
|
108
|
+
|
109
|
+
|
110
|
+
@deployment.command()
|
111
|
+
@click.option('--deployment-id', required=False, default=None)
|
112
|
+
@click.option('--access-key', required=False, default=None)
|
113
|
+
@click.option('--base-url', required=False, default=None)
|
114
|
+
def status(deployment_id: str = None,
|
115
|
+
access_key: str = None,
|
116
|
+
base_url: str = None):
|
117
|
+
""" Check on the status of a Sermos build.
|
118
|
+
|
119
|
+
Arguments:
|
120
|
+
deployment-id (optional): UUID for Deployment. Find in your Sermos
|
121
|
+
Cloud Console. If not provided, looks in environment under
|
122
|
+
`SERMOS_DEPLOYMENT_ID`
|
123
|
+
access-key (optional): Defaults to checking the environment for
|
124
|
+
`SERMOS_ACCESS_KEY`. If not found, will exit.
|
125
|
+
base-url (optional): Defaults to primary Sermos Cloud base URL.
|
126
|
+
Only modify this if there is a specific, known reason to do so.
|
127
|
+
"""
|
128
|
+
# Instantiate SermosDeploy
|
129
|
+
sd = SermosDeploy(deployment_id=deployment_id,
|
130
|
+
access_key=access_key,
|
131
|
+
base_url=base_url)
|
132
|
+
|
133
|
+
# Check deployment status
|
134
|
+
result = sd.get_deployment_status()
|
135
|
+
try:
|
136
|
+
click.echo(result['data']['results'])
|
137
|
+
except Exception as e:
|
138
|
+
logger.error(f"{result} / {e}")
|
pypeline/cloud.py
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
""" Base class for interacting with Sermos Cloud API.
|
2
|
+
"""
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
import requests
|
6
|
+
from pypeline.utils.config_utils import get_access_key, get_deployment_id
|
7
|
+
from pypeline.constants import DEFAULT_BASE_URL, DEPLOYMENTS_DEPLOY_URL,\
|
8
|
+
DEPLOYMENTS_SERVICES_URL
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class SermosCloud():
|
14
|
+
""" Primary Sermos Cloud class for interacting with API.
|
15
|
+
"""
|
16
|
+
def __init__(self,
|
17
|
+
access_key: str = None,
|
18
|
+
base_url: str = None,
|
19
|
+
deployment_id: str = None):
|
20
|
+
""" Arguments:
|
21
|
+
access_key (optional): Access key, issued by Sermos, which is
|
22
|
+
tied to a `Deployment`. Defaults to checking the environment
|
23
|
+
for `SERMOS_ACCESS_KEY`. If not found, will exit.
|
24
|
+
base_url (optional): Defaults to primary Sermos Cloud API
|
25
|
+
endpoint (https://cloud.sermos.ai/api/v1/).
|
26
|
+
Only modify this if there is a specific, known reason to do so.
|
27
|
+
deployment_id: UUID for Deployment. Find in your Sermos
|
28
|
+
Cloud Console.
|
29
|
+
"""
|
30
|
+
super(SermosCloud, self).__init__()
|
31
|
+
self.access_key = get_access_key(access_key)
|
32
|
+
self.base_url = base_url if base_url\
|
33
|
+
else DEFAULT_BASE_URL
|
34
|
+
try:
|
35
|
+
self.deployment_id = get_deployment_id(deployment_id)
|
36
|
+
except KeyError:
|
37
|
+
self.deployment_id = None # Not always required, so allow None ...
|
38
|
+
|
39
|
+
self.deploy_url = DEPLOYMENTS_DEPLOY_URL.format(
|
40
|
+
self.base_url, self.deployment_id)
|
41
|
+
self.services_url = DEPLOYMENTS_SERVICES_URL.format(
|
42
|
+
self.base_url, self.deployment_id)
|
43
|
+
|
44
|
+
# Note: Sermos Cloud's API expects `apikey`
|
45
|
+
self.headers = {
|
46
|
+
'Content-Type': 'application/json',
|
47
|
+
'apikey': self.access_key
|
48
|
+
}
|
49
|
+
|
50
|
+
def get(self, url: str, as_dict: bool = False):
|
51
|
+
""" Send a GET request to Sermos Cloud
|
52
|
+
"""
|
53
|
+
r = requests.get(url, headers=self.headers)
|
54
|
+
if as_dict:
|
55
|
+
return r.json()
|
56
|
+
return r
|
57
|
+
|
58
|
+
def get_all(self, url: str, page: int = 0, page_size: int = 15):
|
59
|
+
""" Loop through all paginated results from a GET endpoint
|
60
|
+
"""
|
61
|
+
new_results = True
|
62
|
+
results = []
|
63
|
+
while new_results:
|
64
|
+
this_url = f"{url}?page={page}&sort_order=DESC&page_size={page_size}"
|
65
|
+
r = requests.get(this_url, headers=self.headers).json()
|
66
|
+
new_results = r.get("data", {}).get("results", [])
|
67
|
+
results.extend(new_results)
|
68
|
+
page += 1
|
69
|
+
|
70
|
+
return {'data': {'results': results}, 'message': 'All Results'}
|
71
|
+
|
72
|
+
def post(self, url: str, payload: dict = None, as_dict: bool = False):
|
73
|
+
""" Send a POST request to Sermos Cloud
|
74
|
+
"""
|
75
|
+
if payload is None:
|
76
|
+
payload = {}
|
77
|
+
r = requests.post(url, headers=self.headers, data=json.dumps(payload))
|
78
|
+
if as_dict:
|
79
|
+
return r.json()
|
80
|
+
return r
|
pypeline/constants.py
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
""" Sermos Constants
|
2
|
+
"""
|
3
|
+
import os
|
4
|
+
from urllib.parse import urljoin
|
5
|
+
|
6
|
+
API_PATH_V1 = '/api/v1'
|
7
|
+
|
8
|
+
DEFAULT_RESULT_TTL = 86400 # seconds (1 day)
|
9
|
+
DEFAULT_TASK_TTL = 60 # seconds (1 minute)
|
10
|
+
DEFAULT_RETRY_TASK_MAX_TTL = 300
|
11
|
+
DEFAULT_MAX_RETRY = 10
|
12
|
+
DEFAULT_REGULATOR_TASK = 'sermos.celery.task_chain_regulator'
|
13
|
+
DEFAULT_SUCCESS_TASK = 'sermos.celery.pipeline_success'
|
14
|
+
DEFAULT_RETRY_TASK = 'sermos.celery.pipeline_retry'
|
15
|
+
|
16
|
+
CHAIN_SUCCESS_MSG = 'Chain built successfully ...'
|
17
|
+
CHAIN_FAILURE_MSG = 'Chain failed to build ...'
|
18
|
+
|
19
|
+
PIPELINE_RUN_WRAPPER_CACHE_KEY = 'sermos_{}_{}' # pipeline_id + execution_id
|
20
|
+
PIPELINE_RESULT_CACHE_KEY = 'sermos_result_{}' # execution_id
|
21
|
+
|
22
|
+
# Pipeline configurations and scheduled task configuration are cached in Redis
|
23
|
+
# temporarily (default to CONFIG_REFRESH_RATE (in seconds)).
|
24
|
+
# Each pipeline config/schedule config is specific to an individual deployment,
|
25
|
+
# however, we cache only with the pipeline_id here because the usage of this
|
26
|
+
# cache key is restricted to the redis instance associated with the deployment.
|
27
|
+
PIPELINE_CONFIG_CACHE_KEY = 'sermos_pipeline_config_{}' # pipeline_id
|
28
|
+
SCHEDULE_CONFIG_CACHE_KEY = 'sermos_schedule_config'
|
29
|
+
CONFIG_REFRESH_RATE = int(os.environ.get('CONFIG_REFRESH_RATE', 30)) # seconds
|
30
|
+
|
31
|
+
# TODO where on earth is this crazy time format coming from?
|
32
|
+
SCHEDULE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
|
33
|
+
|
34
|
+
AUTH_LOCK_KEY = os.environ.get('AUTH_LOCK_KEY', 'sermos-auth-lock')
|
35
|
+
AUTH_LOCK_DURATION = int(os.environ.get('AUTH_LOCK_DURATION', 30))
|
36
|
+
|
37
|
+
STORED_MODEL_KEY = '{}_{}{}'
|
38
|
+
|
39
|
+
# Yaml path is relative to package
|
40
|
+
SERMOS_YAML_PATH = os.environ.get('SERMOS_YAML_PATH', 'sermos.yaml')
|
41
|
+
SERMOS_ACCESS_KEY = os.environ.get('SERMOS_ACCESS_KEY', None)
|
42
|
+
SERMOS_CLIENT_PKG_NAME = os.environ.get('SERMOS_CLIENT_PKG_NAME', None)
|
43
|
+
SERMOS_DEPLOYMENT_ID = os.environ.get('SERMOS_DEPLOYMENT_ID', 'local')
|
44
|
+
LOCAL_DEPLOYMENT_VALUE = os.environ.get('LOCAL_DEPLOYMENT_VALUE', 'local')
|
45
|
+
DEFAULT_BASE_URL = os.environ.get('SERMOS_BASE_URL', 'https://console.sermos.ai')
|
46
|
+
if DEFAULT_BASE_URL != 'local':
|
47
|
+
DEFAULT_BASE_URL += '/api/v1/'
|
48
|
+
DEPLOYMENTS_URL = "{}deployments/{}"
|
49
|
+
DEPLOYMENTS_DEPLOY_URL = "{}deployments/{}/deploy"
|
50
|
+
DEPLOYMENTS_SERVICES_URL = "{}deployments/{}/services"
|
51
|
+
DEPLOYMENTS_SERVICE_URL = "{}deployments/{}/services/{}"
|
52
|
+
DEFAULT_AUTH_URL = urljoin(DEFAULT_BASE_URL, 'auth')
|
53
|
+
USING_SERMOS_CLOUD = DEFAULT_BASE_URL != LOCAL_DEPLOYMENT_VALUE
|
54
|
+
DEFAULT_CONFIG_RETRIEVAL_PAGE_SIZE = 25
|
55
|
+
# Default 'responses' dictionary when decorating endpoints with @api.doc()
|
56
|
+
# Extend as necessary.
|
57
|
+
API_DOC_RESPONSES = {
|
58
|
+
200: {
|
59
|
+
'code': 200,
|
60
|
+
'description': 'Successful response.'
|
61
|
+
},
|
62
|
+
400: {
|
63
|
+
'code': 400,
|
64
|
+
'description': 'Malformed request. Verify payload is correct.'
|
65
|
+
},
|
66
|
+
401: {
|
67
|
+
'code': 401,
|
68
|
+
'description':
|
69
|
+
'Unauthorized. Verify your API Key (`accesskey`) header.'
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
# Default 'params' dictionary when decorating endpoints with @api.doc()
|
74
|
+
# Extend as necessary.
|
75
|
+
API_DOC_PARAMS = {
|
76
|
+
'accesskey': {
|
77
|
+
'in': 'header',
|
78
|
+
'name': 'accesskey',
|
79
|
+
'description': 'Your API Consumer\'s `accesskey`',
|
80
|
+
'type': 'string',
|
81
|
+
'required': True
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
DEFAULT_OPENAPI_CONFIG = (
|
86
|
+
('SWAGGER_UI_DOC_EXPANSION',
|
87
|
+
'list'), ('API_DOCUMENTATION_TITLE',
|
88
|
+
'Sermos API Specs'), ('API_DOCUMENTATION_DESCRIPTION',
|
89
|
+
'Available API Endpoints'),
|
90
|
+
('OPENAPI_VERSION', '3.0.2'), ('OPENAPI_URL_PREFIX',
|
91
|
+
'/api/v1'), ('OPENAPI_SWAGGER_APP_NAME',
|
92
|
+
'Sermos - API Reference'),
|
93
|
+
('OPENAPI_SWAGGER_UI_PATH',
|
94
|
+
'/docs'), ('OPENAPI_SWAGGER_BASE_TEMPLATE',
|
95
|
+
'swagger/swagger_ui.html'), ('OPENAPI_SWAGGER_URL', '/docs'),
|
96
|
+
('OPENAPI_SWAGGER_UI_URL',
|
97
|
+
'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.24.2/'),
|
98
|
+
('EXPLAIN_TEMPLATE_LOADING', False))
|
99
|
+
|
100
|
+
# Rho Auth settings
|
101
|
+
DEFAULT_RHOAUTH_CONFIG = (
|
102
|
+
('RHOAUTH_OIDC_CLIENT_ID',
|
103
|
+
os.environ.get('RHOAUTH_OIDC_CLIENT_ID',
|
104
|
+
os.environ.get('CLIENT_PKG_NAME', '').replace('_', '-'))),
|
105
|
+
('RHOAUTH_OIDC_ISSUER',
|
106
|
+
os.environ.get('RHOAUTH_OIDC_ISSUER',
|
107
|
+
'https://auth.rho.ai/auth/realms/sermos')),
|
108
|
+
('RHOAUTH_OIDC_AUTH_ENDPOINT',
|
109
|
+
os.environ.get(
|
110
|
+
'RHOAUTH_OIDC_AUTH_ENDPOINT',
|
111
|
+
'https://auth.rho.ai/auth/realms/sermos/protocol/openid-connect/auth')
|
112
|
+
),
|
113
|
+
('RHOAUTH_OIDC_TOKEN_ENDPOINT',
|
114
|
+
os.environ.get(
|
115
|
+
'RHOAUTH_OIDC_TOKEN_ENDPOINT',
|
116
|
+
'https://auth.rho.ai/auth/realms/sermos/protocol/openid-connect/token'
|
117
|
+
)),
|
118
|
+
('RHOAUTH_OIDC_JWKS_URI_ENDPOINT',
|
119
|
+
os.environ.get(
|
120
|
+
'RHOAUTH_OIDC_JWKS_URI_ENDPOINT',
|
121
|
+
'https://auth.rho.ai/auth/realms/sermos/protocol/openid-connect/certs'
|
122
|
+
)),
|
123
|
+
('RHOAUTH_OIDC_END_SESSION_ENDPOINT',
|
124
|
+
os.environ.get(
|
125
|
+
'RHOAUTH_OIDC_END_SESSION_ENDPOINT',
|
126
|
+
'https://auth.rho.ai/auth/realms/sermos/protocol/openid-connect/logout'
|
127
|
+
)), ('RHOAUTH_OIDC_END_SESSION_REDIRECT_URI',
|
128
|
+
os.environ.get('RHOAUTH_OIDC_END_SESSION_REDIRECT_URI',
|
129
|
+
'/')), ('OAUTHLIB_INSECURE_TRANSPORT',
|
130
|
+
os.environ.get('OAUTHLIB_INSECURE_TRANSPORT',
|
131
|
+
1)))
|
132
|
+
|
133
|
+
|
134
|
+
def create_model_key(model_prefix: str,
|
135
|
+
model_version: str,
|
136
|
+
model_postfix: str = ''):
|
137
|
+
""" Ensures we're consistently creating the keys for storing/retrieving.
|
138
|
+
"""
|
139
|
+
return STORED_MODEL_KEY.format(model_prefix, model_version, model_postfix)
|
pypeline/deploy.py
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
""" Utilities for deploying applications to Sermos.
|
2
|
+
|
3
|
+
Example CLI Usage::
|
4
|
+
|
5
|
+
$ honcho run -e .env sermos deploy
|
6
|
+
$ honcho run -e .env sermos status
|
7
|
+
|
8
|
+
Example Programmatic Usage::
|
9
|
+
|
10
|
+
from sermos.deploy import SermosDeploy
|
11
|
+
|
12
|
+
sd = SermosDeploy(
|
13
|
+
os.environ.get("SERMOS_ACCESS_KEY", None),
|
14
|
+
pkg_name="sermos_demo_client"
|
15
|
+
)
|
16
|
+
|
17
|
+
# To Invoke
|
18
|
+
status = sd.invoke_deployment()
|
19
|
+
print(status)
|
20
|
+
|
21
|
+
# To Check Status:
|
22
|
+
status = sd.get_deployment_status()
|
23
|
+
print(status)
|
24
|
+
|
25
|
+
"""
|
26
|
+
import subprocess
|
27
|
+
import base64
|
28
|
+
import logging
|
29
|
+
|
30
|
+
from pypeline.utils.module_utils import get_client_pkg_name
|
31
|
+
from pypeline.sermos_yaml import load_sermos_config
|
32
|
+
from pypeline.cloud import SermosCloud
|
33
|
+
|
34
|
+
logger = logging.getLogger(__name__)
|
35
|
+
|
36
|
+
|
37
|
+
class SermosDeploy(SermosCloud):
|
38
|
+
""" Primary Sermos Deployment class for invocation and status updates.
|
39
|
+
"""
|
40
|
+
def __init__(self,
|
41
|
+
deployment_id: str = None,
|
42
|
+
access_key: str = None,
|
43
|
+
pkg_name: str = None,
|
44
|
+
sermos_yaml_filename: str = None,
|
45
|
+
commit_hash: str = None,
|
46
|
+
base_url: str = None,
|
47
|
+
deploy_branch: str = 'main'):
|
48
|
+
""" Arguments:
|
49
|
+
deployment_id (optional): UUID for Deployment. Find in your
|
50
|
+
Sermos Cloud Console.
|
51
|
+
access_key (optional): Access key, issued by Sermos, that
|
52
|
+
dictates the environment into which this request will be
|
53
|
+
deployed. Defaults to checking the environment for
|
54
|
+
`SERMOS_ACCESS_KEY`. If not found, will exit.
|
55
|
+
pkg_name (optional): Directory name for your Python
|
56
|
+
package. e.g. my_package_name . If none provided, will check
|
57
|
+
environment for `SERMOS_CLIENT_PKG_NAME`. If not found,
|
58
|
+
will exit.
|
59
|
+
sermos_yaml_filename (optional): Relative path to find your
|
60
|
+
`sermos.yaml` configuration file. Defaults to `sermos.yaml`
|
61
|
+
which should be found inside your `pkg_name`
|
62
|
+
commit_hash (optional): The specific commit hash of your git
|
63
|
+
repo to deploy. If not provided, then current HEAD as of
|
64
|
+
invocation will be used. This is the default usage, and is
|
65
|
+
useful in the case of a CI/CD pipeline such that the Sermos
|
66
|
+
deployment is invoked after your integration passes.
|
67
|
+
base_url (optional): Defaults to primary Sermos Cloud base URL.
|
68
|
+
Only modify this if there is a specific, known reason to do so.
|
69
|
+
deploy_branch (optional): Defaults to 'main'. Only modify this
|
70
|
+
if there is a specific, known reason to do so.
|
71
|
+
|
72
|
+
"""
|
73
|
+
super().__init__(access_key, base_url, deployment_id)
|
74
|
+
self.pkg_name = get_client_pkg_name(pkg_name)
|
75
|
+
self.sermos_yaml_filename = sermos_yaml_filename
|
76
|
+
self.commit_hash = commit_hash
|
77
|
+
self.sermos_yaml = None # Established later, only on `invoke`
|
78
|
+
self.encoded_sermos_yaml = None # Established later, only on `invoke`
|
79
|
+
self.deploy_payload = None # Established later, only on `invoke`
|
80
|
+
self.deploy_branch = deploy_branch
|
81
|
+
|
82
|
+
def _set_commit_hash(self):
|
83
|
+
""" Retrieve the commit hash of the current git state and set to
|
84
|
+
current deployment object.
|
85
|
+
"""
|
86
|
+
if self.commit_hash is None:
|
87
|
+
self.commit_hash = subprocess.check_output(
|
88
|
+
["git", "rev-parse", "--verify",
|
89
|
+
"HEAD"]).strip().decode('utf-8')
|
90
|
+
|
91
|
+
def _set_encoded_sermos_yaml(self):
|
92
|
+
""" Provide the b64 encoded sermos.yaml file as part of request.
|
93
|
+
Primarily used to get the custom workers definitions, etc. so
|
94
|
+
the deployment endpoint can generate the values.yaml.
|
95
|
+
"""
|
96
|
+
self.sermos_yaml = load_sermos_config(self.pkg_name,
|
97
|
+
self.sermos_yaml_filename,
|
98
|
+
as_dict=False)
|
99
|
+
self.encoded_sermos_yaml = base64.b64encode(
|
100
|
+
self.sermos_yaml.encode('utf-8')).decode('utf-8')
|
101
|
+
|
102
|
+
def _set_deploy_payload(self):
|
103
|
+
""" Set the deployment payload correctly.
|
104
|
+
"""
|
105
|
+
self._set_commit_hash()
|
106
|
+
self._set_encoded_sermos_yaml()
|
107
|
+
self.deploy_payload = {
|
108
|
+
"sermos_yaml": self.encoded_sermos_yaml,
|
109
|
+
"commit_hash": self.commit_hash,
|
110
|
+
"deploy_branch": self.deploy_branch,
|
111
|
+
"client_package_name": self.pkg_name
|
112
|
+
}
|
113
|
+
|
114
|
+
def get_deployment_status(self):
|
115
|
+
""" Info on a specific deployment
|
116
|
+
"""
|
117
|
+
resp = self.get(self.services_url)
|
118
|
+
services = resp.json().get('data', {}).get('results', [])
|
119
|
+
status_map = {
|
120
|
+
'data': {
|
121
|
+
'results': []
|
122
|
+
},
|
123
|
+
'message': 'Status of all Deployment Services'
|
124
|
+
}
|
125
|
+
for service in services:
|
126
|
+
status_map['data']['results'].append({
|
127
|
+
'service_id':
|
128
|
+
service['niceId'],
|
129
|
+
'name':
|
130
|
+
service['name'],
|
131
|
+
'status':
|
132
|
+
service['status']
|
133
|
+
})
|
134
|
+
return status_map
|
135
|
+
|
136
|
+
def validate_deployment(self, output_file: str = None):
|
137
|
+
""" Test rendering sermos.yaml and validate.
|
138
|
+
"""
|
139
|
+
# Running this will raise an exception if something is invalid.
|
140
|
+
self._set_deploy_payload()
|
141
|
+
|
142
|
+
if output_file:
|
143
|
+
with open(output_file, 'w') as f:
|
144
|
+
f.write(self.sermos_yaml)
|
145
|
+
|
146
|
+
# Additional validation based on logical requirements (validation of
|
147
|
+
# the schemaa is handled by Marshmallow but there can be 'valid' entries
|
148
|
+
# that would result in a failed deployment, such as an invalid
|
149
|
+
# application under appConfig.appPath, for example)
|
150
|
+
|
151
|
+
return True
|
152
|
+
|
153
|
+
def invoke_deployment(self):
|
154
|
+
""" Invoke a Sermos AI Deployment
|
155
|
+
|
156
|
+
If no commit_hash was provided, use the "current" commit hash
|
157
|
+
of the client package during this invocation.
|
158
|
+
|
159
|
+
Required convention is that your client's python package
|
160
|
+
version number is specified in the file `my_package/__init__.py`
|
161
|
+
and is defined as a string assigned to the variable `__version__`,
|
162
|
+
e.g. `__version__ = '0.1.0'`
|
163
|
+
"""
|
164
|
+
self._set_deploy_payload()
|
165
|
+
|
166
|
+
# Make request to your environment's endpoint
|
167
|
+
return self.post(self.deploy_url, self.deploy_payload)
|
pypeline/extensions.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
""" Initialize most extensions used throughout application
|
2
|
+
"""
|
3
|
+
import logging
|
4
|
+
|
5
|
+
logger = logging.getLogger(__name__)
|
6
|
+
|
7
|
+
try:
|
8
|
+
# Client packages *should* provide a `sermos.yaml` file. This
|
9
|
+
# loads the configuration file with the provided name ofthe client
|
10
|
+
# package (e.g. sermos_demo_client)
|
11
|
+
from pypeline.sermos_yaml import load_client_config_and_version
|
12
|
+
sermos_config, sermos_client_version = load_client_config_and_version()
|
13
|
+
except Exception as e:
|
14
|
+
sermos_config = None
|
15
|
+
sermos_client_version = None
|
16
|
+
logger.warning("Unable to load client Sermos config ... {}".format(e))
|
@@ -0,0 +1,28 @@
|
|
1
|
+
""" Sermos' Flask Implementation and Tooling. Convenience imports here.
|
2
|
+
"""
|
3
|
+
import logging
|
4
|
+
|
5
|
+
logger = logging.getLogger(__name__)
|
6
|
+
|
7
|
+
try:
|
8
|
+
from rho_web.smorest import Blueprint, Api
|
9
|
+
from rho_web.response import abort
|
10
|
+
except Exception as e:
|
11
|
+
logger.error("Unable to import Web services (Blueprint, API, abort)"
|
12
|
+
f" ... {e}")
|
13
|
+
|
14
|
+
try:
|
15
|
+
from flask_rhoauth import OpenIDConnect
|
16
|
+
except Exception as e:
|
17
|
+
oidc = None
|
18
|
+
OpenIDConnect = None
|
19
|
+
logging.info("Did not initialize oidc ... {}".format(e))
|
20
|
+
else:
|
21
|
+
if OpenIDConnect is not None:
|
22
|
+
oidc = OpenIDConnect()
|
23
|
+
|
24
|
+
try:
|
25
|
+
from pypeline.flask.flask_sermos import FlaskSermos
|
26
|
+
except Exception as e:
|
27
|
+
logger.exception("Unable to import Sermos services (FlaskSermos)"
|
28
|
+
f" ... {e}")
|
File without changes
|