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,737 @@
1
+ """ Definition of the `sermos.yaml` file. This is only relevant/used for
2
+ managed deployments through Sermos.ai. If self-hosting, safely disregard this
3
+ yaml format, no `sermos.yaml` is required for your application.
4
+
5
+ If using, a basic file may look like::
6
+
7
+ imageConfig:
8
+ - name: base-image
9
+ installCommand: sermos-demo-client[core]
10
+ - name: public-api-image
11
+ repositoryUrl: myregistry/public-api-image
12
+
13
+ environmentVariables:
14
+ - name: GLOBAL_ENV_VAR
15
+ value: globally-available-env-var
16
+
17
+ serviceConfig:
18
+ - name: base-worker
19
+ serviceType: celery-worker
20
+ imageName: base-image
21
+ queue: default-queue
22
+ registeredTasks:
23
+ - handler: sermos_demo_client.workers.demo_worker.demo_task
24
+
25
+
26
+ imageConfig:
27
+ - name: base-image
28
+ installCommand: sermos-demo-client
29
+ sourceSshUrl: git@gitlab.com:sermos/sermos-demo-client.git
30
+ baseImage: registry.gitlab.com/sermos/sermos-tools:0.3.0
31
+
32
+ serviceConfig:
33
+ - name: demo-api
34
+ serviceType: external
35
+ serviceId: ${SERVICE_ID_API} # Rendered using `sermos deploy` if available in the environment.
36
+ imageName: base-image
37
+ command: gunicorn --log-level info -k gevent -b 0.0.0.0:5000 sermos_demo_client.app:create_app()
38
+ port: 5000
39
+ replicaCount: 1
40
+ cpuLimit: 0.5
41
+ memoryLimit: 0.5
42
+ environmentVariables:
43
+ - name: FLASK_SECRET_KEY
44
+ value: ${FLASK_SECRET_KEY}
45
+ - name: sermos-worker
46
+ serviceType: celery-worker
47
+ serviceId: ${SERVICE_ID_WORKER}
48
+ imageName: base-image
49
+ command: celery -A sermos_demo_client.celery worker --without-gossip --without-mingle -c '1' -l INFO --queue default-task-queue
50
+ replicaCount: 1
51
+ cpuLimit: 0.5
52
+ memoryLimit: 0.5
53
+ registeredTasks:
54
+ - handler: sermos_demo_client.workers.demo_worker.demo_worker_task
55
+ - handler: sermos_demo_client.workers.demo_worker.demo_model_task
56
+ environmentVariables:
57
+ - name: WORKER_NAME
58
+ value: sermos-worker
59
+
60
+ pipelines:
61
+ demo-pipeline:
62
+ name: demo-pipeline
63
+ description: Demo Pipeline.
64
+ schemaVersion: 1
65
+ config:
66
+ dagAdjacency:
67
+ node_a:
68
+ - node_b
69
+ - node_c
70
+ metadata:
71
+ maxRetry: 3
72
+ maxTtl: 60
73
+ queue: default-task-queue
74
+ taskDefinitions:
75
+ node_a:
76
+ handler: sermos_demo_client.workers.demo_pipeline.demo_pipeline_node_a
77
+ node_b:
78
+ handler: sermos_demo_client.workers.demo_pipeline.demo_pipeline_node_b
79
+ queue: node-b-queue
80
+ node_c:
81
+ handler: sermos_demo_client.workers.demo_pipeline.demo_pipeline_node_c
82
+
83
+ scheduledTasks:
84
+ demo-model-task:
85
+ name: Demo Model Task
86
+ enabled: true
87
+ config:
88
+ scheduleType: interval
89
+ task: sermos_demo_client.workers.demo_worker.demo_model_task
90
+ queue: default-task-queue
91
+ schedule:
92
+ every: 60
93
+ period: seconds
94
+ schemaVersion: 1
95
+
96
+ """
97
+ import re
98
+ import os
99
+ import logging
100
+ import pkg_resources
101
+ import yaml
102
+ from yaml.loader import SafeLoader
103
+ from marshmallow import Schema, fields, pre_load, EXCLUDE, INCLUDE,\
104
+ validates_schema
105
+ from marshmallow.validate import OneOf
106
+ from marshmallow.exceptions import ValidationError
107
+ from pypeline.utils.module_utils import SermosModuleLoader, normalized_pkg_name
108
+ from pypeline.constants import SERMOS_YAML_PATH, SERMOS_CLIENT_PKG_NAME
109
+ from pypeline.pipeline_config_schema import BasePipelineSchema
110
+ from pypeline.schedule_config_schema import BaseScheduleSchema
111
+
112
+ logger = logging.getLogger(__name__)
113
+
114
+
115
+ class InvalidPackagePath(Exception):
116
+ pass
117
+
118
+
119
+ class InvalidSermosConfig(Exception):
120
+ pass
121
+
122
+
123
+ class MissingSermosConfig(Exception):
124
+ pass
125
+
126
+
127
+ class InvalidImageConfig(Exception):
128
+ pass
129
+
130
+
131
+ class ExcludeUnknownSchema(Schema):
132
+ class Meta:
133
+ unknown = EXCLUDE
134
+
135
+
136
+ class EnvironmentVariableSchema(ExcludeUnknownSchema):
137
+ """ A single environment variables (singular)
138
+ """
139
+ name = fields.String(required=True,
140
+ description="Environment variable name.",
141
+ example="MY_ENV_VAR")
142
+ value = fields.String(required=True,
143
+ description="Environment variable value.",
144
+ example="my special value")
145
+
146
+
147
+ class EnvironmentVariablesSchema(Schema):
148
+ """ Multiple environment variables (plural)
149
+ """
150
+ environmentVariables = fields.List(
151
+ fields.Nested(EnvironmentVariableSchema, required=True),
152
+ description="List of name/value environment variable pairs available "
153
+ "to the scope of this service.",
154
+ required=False)
155
+
156
+
157
+ class ServiceRequestsSchema(Schema):
158
+
159
+ replicaCount = fields.Integer(
160
+ required=False,
161
+ description="Baseline (min) scale of this service to have available.",
162
+ default=1,
163
+ example=1)
164
+ cpuRequest = fields.Float(
165
+ required=False,
166
+ description="Requested CPUs to be available for each replica.",
167
+ default=0.5,
168
+ example="0.5")
169
+ memoryRequest = fields.Float(
170
+ required=False,
171
+ description="Requested memory (in GB) to be available for each replica.",
172
+ default=0.5,
173
+ example="0.5 (means half of 1 GB)")
174
+ ephemeralStorageRequest = fields.Float(
175
+ required=False,
176
+ description="Requested ephemeral storage (in GB) to "
177
+ "be available for each replica.",
178
+ default=8,
179
+ example="2 (means 2 GB)")
180
+ cpuLimit = fields.Float(
181
+ required=False,
182
+ description="Maximum CPUs to be available for each replica.",
183
+ default=0.5,
184
+ example="0.5")
185
+ memoryLimit = fields.Float(
186
+ required=False,
187
+ description="Maximum memory (in GB) to be available for each replica.",
188
+ default=0.5,
189
+ example="0.5 (means half of 1 GB)")
190
+ ephemeralStorageLimit = fields.Float(
191
+ required=False,
192
+ description="Maximum ephemeral storage (in GB) to "
193
+ "be available for each replica.",
194
+ default=8,
195
+ example="2 (means 2 GB)")
196
+
197
+
198
+ class NameSchema(Schema):
199
+ """ Validated name string field.
200
+ """
201
+ name = fields.String(
202
+ required=True,
203
+ description="Name for service or image. Must include "
204
+ "only alphanumeric characters along with `_` and `-`.",
205
+ example="my-service-name")
206
+
207
+ @pre_load
208
+ def validate_characters(self, item, **kwargs):
209
+ """ Ensure name field conforms to allowed characters
210
+ """
211
+ valid_chars = r'^[\w\d\-\_]+$'
212
+ if not bool(re.match(valid_chars, item['name'])):
213
+ raise ValueError(
214
+ f"Invalid name: {item['name']}. Only alphanumeric characters "
215
+ "allowed along with `-` and `_`.")
216
+ return item
217
+
218
+
219
+ class SermosImageConfigSchema(NameSchema):
220
+ installCommand = fields.String(
221
+ required=False,
222
+ description="The pip install command to use when Sermos is "
223
+ "responsible for building your image.",
224
+ example="sermos-client-pkg[core,special_feature]")
225
+ sourceSshUrl = fields.String(
226
+ required=False,
227
+ description="The source code ssh url to use when Sermos is "
228
+ "responsible for building your image.",
229
+ example="git@github.com:myorg/sermos-client.git")
230
+ baseImage = fields.String(
231
+ required=False,
232
+ description="The Docker base image to use as the starting point when "
233
+ "Sermos is responsible for building your image",
234
+ example="rhoai/sermos:latest")
235
+ repositoryUrl = fields.String(
236
+ required=False,
237
+ description="Deprecated - Use imageUri instead.")
238
+ repositoryUser = fields.String(
239
+ required=False,
240
+ description="Deprecated - Use registryUser instead.",)
241
+ repositoryPassword = fields.String(
242
+ required=False,
243
+ description="Deprecated - Use registryPassword instead.")
244
+ imageUri = fields.String(
245
+ required=False,
246
+ description="The Docker image uri when using an image not built by "
247
+ "Sermos. Tag is optional, if excluded, `latest` is used.",
248
+ example="rhoai/custom-image ; rhoai/custom-image:v0.0.0")
249
+ registryDomain = fields.String(
250
+ required=False,
251
+ description="Registry domain for using a pre-built image.",
252
+ example="index.docker.io")
253
+ registryUser = fields.String(
254
+ required=False,
255
+ description="Optional registry username if provided registryDomain"
256
+ "is a private registry.",
257
+ example="rhoai")
258
+ registryPassword = fields.String(
259
+ required=False,
260
+ description="Optional registry password if provided registryDomain "
261
+ "is a private registry. NOTE: Strongly recommended to use environment "
262
+ "variable interpolation in your sermos.yaml file, do not commit "
263
+ "unencrypted secrets to a git repository. "
264
+ "e.g. repositoryPassword: ${DOCKER_REPOSITORY_PASSWORD}",
265
+ example="abc123")
266
+
267
+
268
+ class SermosSharedConfigSchema(Schema):
269
+ """ Attributes shared across internal and external service types
270
+ """
271
+ command = fields.String(
272
+ required=False,
273
+ _required=True,
274
+ description="Command to be run as container CMD.",
275
+ example="gunicorn -b 0.0.0.0:5000 package.app:create_app()")
276
+ port = fields.Integer(
277
+ required=False,
278
+ _required=True,
279
+ description="Port (and targetPort) to direct traffic.",
280
+ example=5000)
281
+
282
+
283
+ class SermosExternalConfigSchema(SermosSharedConfigSchema):
284
+ """ Attributes required for serviceType: external
285
+
286
+ Note: Schema lists these are not required in order for this to be used
287
+ as a mixin to SermosServiceConfigSchema. The validation is done
288
+ programmatically based on serviceType.
289
+ """
290
+ serviceId = fields.String(
291
+ required=False,
292
+ _required=True,
293
+ description="The serviceId provided by Sermos. Find in admin console.",
294
+ example="dry-gorge-8018")
295
+
296
+
297
+ class SermosInternalSchema(SermosSharedConfigSchema):
298
+ """ Attributes required for serviceType: internal
299
+
300
+ Note: Schema lists these are not required in order for this to be used
301
+ as a mixin to SermosServiceConfigSchema. The validation is done
302
+ programmatically based on serviceType.
303
+ """
304
+ protocol = fields.String(required=False,
305
+ _required=True,
306
+ description="Protocol to use.",
307
+ example="TCP",
308
+ validate=OneOf(['TCP', 'UDP']))
309
+
310
+
311
+ class SermosRegisteredTaskDetailConfigSchema(Schema):
312
+ handler = fields.String(
313
+ required=True,
314
+ description="Full path to the Method handles work / pipeline tasks.",
315
+ example="sermos_customer_client.workers.worker_group.useful_worker")
316
+
317
+ event = fields.Raw(
318
+ required=False,
319
+ unknown=INCLUDE,
320
+ description="Arbitrary user data, passed through `event` arg in task.")
321
+
322
+
323
+ class SermosCeleryWorkerConfigSchema(Schema):
324
+ """ Attributes required for serviceType: celery-worker
325
+
326
+ Note: Schema lists these are not required in order for this to be used
327
+ as a mixin to SermosServiceConfigSchema. The validation is done
328
+ programmatically based on serviceType.
329
+ """
330
+ command = fields.String(
331
+ required=False,
332
+ _required=True,
333
+ description="Command to be run as container CMD.",
334
+ example="celery -A mypkg.celery.celery worker --queue my-queue")
335
+
336
+ registeredTasks = fields.List(
337
+ fields.Nested(SermosRegisteredTaskDetailConfigSchema, required=True),
338
+ required=False,
339
+ _required=True,
340
+ description="List of task handlers to register for to your Sermos app."
341
+ )
342
+
343
+
344
+ # Mapping of serviceType keys to their respective schema
345
+ service_types = {
346
+ 'external': SermosExternalConfigSchema,
347
+ 'internal': SermosInternalSchema,
348
+ 'celery-worker': SermosCeleryWorkerConfigSchema
349
+ }
350
+
351
+
352
+ class SermosServiceConfigSchema(ExcludeUnknownSchema, ServiceRequestsSchema,
353
+ EnvironmentVariablesSchema,
354
+ SermosExternalConfigSchema,
355
+ SermosInternalSchema,
356
+ SermosCeleryWorkerConfigSchema, NameSchema):
357
+ """ Base service config object definition for workers and internal/external
358
+ services.
359
+ """
360
+ imageName = fields.String(
361
+ required=True,
362
+ description="Specify the name of the base image to use for this "
363
+ "service.",
364
+ example="custom-worker-name")
365
+
366
+ serviceType = fields.String(required=True,
367
+ description="Name of the worker.",
368
+ example="useful_worker",
369
+ validate=OneOf(service_types.keys()))
370
+
371
+
372
+ class SermosYamlSchema(ExcludeUnknownSchema, EnvironmentVariablesSchema):
373
+ """ The primary `sermos.yaml` file schema. This defines all available
374
+ properties in a valid Sermos configuration file.
375
+ """
376
+
377
+ imageConfig = fields.List(fields.Nested(SermosImageConfigSchema,
378
+ required=True),
379
+ required=True,
380
+ description="List of available base images. At "
381
+ "least one image must be defined. The 'name' "
382
+ "of the image is used in each service defined "
383
+ "in `serviceConfig`")
384
+
385
+ serviceConfig = fields.List(
386
+ fields.Nested(SermosServiceConfigSchema,
387
+ required=True,
388
+ description="Core service configuration."),
389
+ description="List of workers for Sermos to manage.",
390
+ required=True)
391
+
392
+ pipelines = fields.Dict(keys=fields.String(),
393
+ values=fields.Nested(BasePipelineSchema),
394
+ description="List of pipelines",
395
+ required=False)
396
+
397
+ scheduledTasks = fields.Dict(keys=fields.String(),
398
+ values=fields.Nested(BaseScheduleSchema),
399
+ description="List of scheduled tasks",
400
+ required=False)
401
+
402
+ def validate_errors(self, schema: Schema, value: dict):
403
+ """ Run Marshmallow validate() and raise if any errors
404
+ """
405
+ schema = schema()
406
+ errors = schema.validate(value)
407
+ if len(errors.keys()) > 0:
408
+ raise ValidationError(errors)
409
+
410
+ @validates_schema
411
+ def validate_schema(self, data, **kwargs):
412
+ """ Additional validation.
413
+
414
+ Nested fields that are not required are not validated by Marshmallow
415
+ by default. Do a single level down of validation for now.
416
+
417
+ Each serviceType has attributes that are required but are listed
418
+ as not required in the marshmallow schema. Validate here.
419
+
420
+ imageConfig can provide *either* an install command for Sermos
421
+ to use to build the image for customer *or* a Docker repository
422
+ for Sermos to pull.
423
+ """
424
+ # Vaidate nested
425
+ key_schema_pairs = (
426
+ ('imageConfig', SermosImageConfigSchema),
427
+ ('environmentVariables', EnvironmentVariableSchema),
428
+ ('serviceConfig', SermosServiceConfigSchema),
429
+ )
430
+ for k_s in key_schema_pairs:
431
+ val = data.get(k_s[0], None)
432
+ if val is not None:
433
+ if type(val) == list:
434
+ for v in val:
435
+ self.validate_errors(k_s[1], v)
436
+ else:
437
+ self.validate_errors(k_s[1], val)
438
+
439
+ # Validate the services. We list every service schema field as not
440
+ # required in order to use them as mixins for a generic service object,
441
+ # however, they ARE required, so validate here using the custom
442
+ # metadata property `_required`. Default to value of `required`.
443
+ service_image_names = []
444
+ for service in data.get('serviceConfig'):
445
+ service_type = service['serviceType']
446
+ schema = service_types[service_type]
447
+ for field in schema().fields:
448
+ try:
449
+ if schema().fields[field].metadata.get(
450
+ '_required',
451
+ getattr(schema().fields[field], 'required')):
452
+ assert field in service
453
+ except AssertionError:
454
+ raise ValidationError(
455
+ f"`{field}` missing in {service_type} definition.")
456
+
457
+ service_image_names.append(service['imageName'])
458
+
459
+ # Validate imageConfig
460
+ image_names = []
461
+ for image in data.get('imageConfig'):
462
+ image_names.append(image['name'])
463
+ try:
464
+ if image.get('installCommand', None) is not None:
465
+ assert image.get('repositoryUrl', None) is None
466
+ if image.get('repositoryUrl', None) is not None:
467
+ assert image.get('installCommand', None) is None
468
+ except AssertionError:
469
+ raise InvalidImageConfig(
470
+ "Each imageConfig may have *either* an installCommand "
471
+ "*or* a repositoryUrl but not both. Review "
472
+ f"image `{image['name']}`")
473
+
474
+ if image.get('installCommand', None) is not None:
475
+ if image.get('sourceSshUrl') is None:
476
+ raise InvalidImageConfig(
477
+ "Each imageConfig must have a sourceSshUrl if "
478
+ "installCommand is specified")
479
+ if image.get('baseImage') is None:
480
+ raise InvalidImageConfig(
481
+ "Each imageConfig must have a baseImage if "
482
+ "installCommand is specified")
483
+
484
+ # Verify each imageName referenced in each service exists in imageConfig
485
+ try:
486
+ assert set(service_image_names).issubset(image_names)
487
+ except AssertionError:
488
+ raise InvalidImageConfig(
489
+ "Mismatched imageName in at least one "
490
+ f"service ({service_image_names}) compared to images available "
491
+ f"in imageConfig ({image_names})")
492
+
493
+ # Validate unique pipeline ids
494
+ if 'pipelines' in data:
495
+ pipeline_ids = set()
496
+ for pipeline_id, pipeline_data in data['pipelines'].items():
497
+ if pipeline_id in pipeline_ids:
498
+ raise ValidationError("All pipeline ids must be unique!")
499
+ pipeline_ids.add(pipeline_id)
500
+ schema_version = pipeline_data['schemaVersion']
501
+ PipelineSchema = \
502
+ BasePipelineSchema.get_by_version(schema_version)
503
+ self.validate_errors(PipelineSchema, pipeline_data)
504
+
505
+ # Validate unique scheduled tasks names
506
+ if 'scheduledTasks' in data:
507
+ task_ids = set()
508
+ for task_id, task_data in data['scheduledTasks'].items():
509
+ if task_id in task_ids:
510
+ raise ValidationError("All schedule ids must be unique!")
511
+ task_ids.add(task_id)
512
+ schema_version = task_data['schemaVersion']
513
+ TaskSchema = BaseScheduleSchema.get_by_version(schema_version)
514
+ self.validate_errors(TaskSchema, task_data)
515
+
516
+
517
+ class YamlPatternConstructor():
518
+ """ Adds a pattern resolver + constructor to PyYaml.
519
+
520
+ Typical/deault usage is for parsing environment variables
521
+ in a yaml file but this can be used for any pattern you provide.
522
+
523
+ See: https://pyyaml.org/wiki/PyYAMLDocumentation
524
+ """
525
+ def __init__(self,
526
+ env_var_pattern: str = None,
527
+ add_constructor: bool = True):
528
+ self.env_var_pattern = env_var_pattern
529
+ if self.env_var_pattern is None:
530
+ # Default pattern is: ${VAR:default}
531
+ self.env_var_pattern = r'^\$\{(.*)\}$'
532
+ self.path_matcher = re.compile(self.env_var_pattern)
533
+
534
+ if add_constructor:
535
+ self.add_constructor()
536
+
537
+ def _path_constructor(self, loader, node):
538
+ """ Extract the matched value, expand env variable,
539
+ and replace the match
540
+
541
+ TODO: Would need to update this (specifically the parsing) if any
542
+ pattern other than our default (or a highly compatible variation)
543
+ is provided.
544
+ """
545
+ # Try to match the correct env variable pattern in this node's value
546
+ # If the value does not match the pattern, return None (which means
547
+ # this node will not be parsed for ENV variables and instead just
548
+ # returned as-is).
549
+ env_var_name = re.match(self.env_var_pattern, node.value)
550
+ try:
551
+ env_var_name = env_var_name.group(1)
552
+ except AttributeError:
553
+ return None
554
+
555
+ # If we get down here, then the 'node.value' matches our specified
556
+ # pattern, so try to parse. env_var_name is the value inside ${...}.
557
+ # Split on `:`, which is our delimiter for default values.
558
+ env_var_name_split = env_var_name.split(':')
559
+
560
+ # Attempt to retrieve the environment variable...from the environment
561
+ env_var = os.environ.get(env_var_name_split[0], None)
562
+
563
+ if env_var is None: # Nothing found in environment
564
+ # If a default was provided (e.g. VAR:default), return that.
565
+ # We join anything after first element because the default
566
+ # value might be a URL or something with a colon in it
567
+ # which would have 'split' above
568
+ if len(env_var_name_split) > 1:
569
+ return ":".join(env_var_name_split[1:])
570
+ return 'unset' # Return 'unset' if not in environ nor default
571
+ return env_var
572
+
573
+ def add_constructor(self):
574
+ """ Initialize PyYaml with ability to resolve/load environment
575
+ variables defined in a yaml template when they exist in
576
+ the environment.
577
+
578
+ Add to SafeLoader in addition to standard Loader.
579
+ """
580
+ # Add the `!env_var` tag to any scalar (value) that matches the
581
+ # pattern self.path_matcher. This allows the template to be much more
582
+ # intuitive vs needing to add !env_var to the beginning of each value
583
+ yaml.add_implicit_resolver('!env_var', self.path_matcher)
584
+ yaml.add_implicit_resolver('!env_var',
585
+ self.path_matcher,
586
+ Loader=SafeLoader)
587
+
588
+ # Add constructor for the tag `!env_var`, which is a function that
589
+ # converts a node of a YAML representation graph to a native Python
590
+ # object.
591
+ yaml.add_constructor('!env_var', self._path_constructor)
592
+ yaml.add_constructor('!env_var',
593
+ self._path_constructor,
594
+ Loader=SafeLoader)
595
+
596
+
597
+ def parse_config_file(sermos_yaml: str):
598
+ """ Parse the `sermos.yaml` file when it's been loaded.
599
+
600
+ Arguments:
601
+ sermos_yaml (required): String of loaded sermos.yaml file.
602
+ """
603
+ YamlPatternConstructor() # Add our env variable parser
604
+ try:
605
+ sermos_yaml_schema = SermosYamlSchema()
606
+ # First suss out yaml issues
607
+ sermos_config = yaml.safe_load(sermos_yaml)
608
+ # Then schema issues
609
+ sermos_config = sermos_yaml_schema.load(sermos_config)
610
+ except ValidationError as e:
611
+ msg = "Invalid Sermos configuration due to {}"\
612
+ .format(e.messages)
613
+ logger.error(msg)
614
+ raise InvalidSermosConfig(msg)
615
+ except InvalidImageConfig as e:
616
+ msg = "Invalid imageConfig configuration due to {}"\
617
+ .format(e)
618
+ logger.error(msg)
619
+ raise InvalidImageConfig(msg)
620
+ except Exception as e:
621
+ msg = "Invalid Sermos configuration, likely due to invalid "\
622
+ "YAML formatting ..."
623
+ logger.exception("{} {}".format(msg, e))
624
+ raise InvalidSermosConfig(msg)
625
+ return sermos_config
626
+
627
+
628
+ def _get_pkg_name(pkg_name: str) -> str:
629
+ """ Retrieve the normalized package name.
630
+ """
631
+ if pkg_name is None:
632
+ pkg_name = SERMOS_CLIENT_PKG_NAME # From environment
633
+ if pkg_name is None:
634
+ return None
635
+ return normalized_pkg_name(pkg_name)
636
+
637
+
638
+ def load_sermos_config(pkg_name: str = None,
639
+ sermos_yaml_filename: str = None,
640
+ as_dict: bool = True):
641
+ """ Load and parse the `sermos.yaml` file. Issue usable exceptions for
642
+ known error modes so bootstrapping can handle appropriately.
643
+
644
+ Arguments:
645
+ pkg_name (required): Directory name for your Python
646
+ package. e.g. my_package_name . If none provided, will check
647
+ environment for `SERMOS_CLIENT_PKG_NAME`. If not found,
648
+ will exit.
649
+ sermos_yaml_filename (optional): Relative path to find your
650
+ `sermos.yaml` configuration file. Defaults to `sermos.yaml`
651
+ which should be found inside your `pkg_name`
652
+ as_dict (optional): If true (default), return the loaded sermos
653
+ configuration as a dictionary. If false, return the loaded
654
+ string value of the yaml file.
655
+ """
656
+ if sermos_yaml_filename is None:
657
+ sermos_yaml_filename = SERMOS_YAML_PATH
658
+
659
+ logger.info(f"Loading `sermos.yaml` from package `{pkg_name}` "
660
+ f"and file location `{sermos_yaml_filename}` ...")
661
+ sermos_config = None
662
+
663
+ pkg_name = _get_pkg_name(pkg_name)
664
+ if pkg_name is None: # Nothing to retrieve at this point
665
+ logger.warning("Unable to retrieve sermos.yaml configuration ...")
666
+ return sermos_config
667
+
668
+ try:
669
+ sermos_config_path = pkg_resources.resource_filename(
670
+ pkg_name, sermos_yaml_filename)
671
+ except Exception as e:
672
+ msg = "Either pkg_name ({}) or sermos_yaml_filename ({}) is "\
673
+ "invalid ...".format(pkg_name, sermos_yaml_filename)
674
+ logger.error("{} ... {}".format(msg, e))
675
+ raise InvalidPackagePath(e)
676
+
677
+ try:
678
+ with open(sermos_config_path, 'r') as f:
679
+ sermos_yaml = f.read()
680
+ sermos_config = parse_config_file(sermos_yaml)
681
+ except InvalidSermosConfig as e:
682
+ raise
683
+ except InvalidImageConfig as e:
684
+ raise
685
+ except FileNotFoundError as e:
686
+ msg = "Sermos config file could not be found at path {} ...".format(
687
+ sermos_config_path)
688
+ raise MissingSermosConfig(msg)
689
+ except Exception as e:
690
+ raise e
691
+ if as_dict:
692
+ return sermos_config
693
+ return yaml.safe_dump(sermos_config)
694
+
695
+
696
+ def load_client_config_and_version(pkg_name: str = None,
697
+ sermos_yaml_filename: str = None):
698
+ """ Load and parse the `sermos.yaml` file and a client package's version.
699
+
700
+ Arguments:
701
+ pkg_name (required): Directory name for your Python
702
+ package. e.g. my_package_name . If none provided, will check
703
+ environment for `SERMOS_CLIENT_PKG_NAME`. If not found,
704
+ will exit.
705
+ sermos_yaml_filename (optional): Relative path to find your
706
+ `sermos.yaml` configuration file. Defaults to `sermos.yaml`
707
+ which should be found inside your `pkg_name`
708
+ as_dict (optional): If true (default), return the loaded sermos
709
+ configuration as a dictionary. If false, return the loaded
710
+ string value of the yaml file.
711
+
712
+ For this to work properly, the provided package must be installed in the
713
+ same environment as this Sermos package and it must have a `__version__`
714
+ variable inside its `__init__.py` file, e.g. `__version__ = '0.0.0'`
715
+ """
716
+ sermos_config = None
717
+ client_version = None
718
+
719
+ pkg_name = _get_pkg_name(pkg_name)
720
+
721
+ try:
722
+ loader = SermosModuleLoader()
723
+ pkg = loader.get_module(pkg_name + '.__init__')
724
+ client_version = getattr(pkg, '__version__', '0.0.0')
725
+ sermos_config = load_sermos_config(pkg_name, sermos_yaml_filename)
726
+ except MissingSermosConfig as e:
727
+ logger.error(e)
728
+ except InvalidSermosConfig as e:
729
+ logger.error(e)
730
+ except InvalidPackagePath as e:
731
+ logger.error(e)
732
+ except Exception as e:
733
+ logger.error("Unable to load client's pkg __version__ or "
734
+ "{} config file for package: {} ... {}".format(
735
+ sermos_yaml_filename, pkg_name, e))
736
+
737
+ return sermos_config, client_version