scalable-pypeline 1.2.3__py2.py3-none-any.whl → 2.0.1__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.
Files changed (41) hide show
  1. pypeline/__init__.py +1 -1
  2. pypeline/barrier.py +34 -0
  3. pypeline/composition.py +348 -0
  4. pypeline/constants.py +51 -84
  5. pypeline/dramatiq.py +470 -0
  6. pypeline/extensions.py +9 -8
  7. pypeline/flask/__init__.py +3 -5
  8. pypeline/flask/api/pipelines.py +109 -148
  9. pypeline/flask/api/schedules.py +14 -39
  10. pypeline/flask/decorators.py +18 -53
  11. pypeline/flask/flask_pypeline.py +156 -0
  12. pypeline/middleware.py +61 -0
  13. pypeline/pipeline_config_schema.py +105 -92
  14. pypeline/pypeline_yaml.py +458 -0
  15. pypeline/schedule_config_schema.py +35 -120
  16. pypeline/utils/config_utils.py +52 -310
  17. pypeline/utils/module_utils.py +35 -71
  18. pypeline/utils/pipeline_utils.py +161 -0
  19. scalable_pypeline-2.0.1.dist-info/METADATA +217 -0
  20. scalable_pypeline-2.0.1.dist-info/RECORD +27 -0
  21. scalable_pypeline-2.0.1.dist-info/entry_points.txt +3 -0
  22. tests/fixtures/__init__.py +0 -1
  23. pypeline/celery.py +0 -206
  24. pypeline/celery_beat.py +0 -254
  25. pypeline/flask/api/utils.py +0 -35
  26. pypeline/flask/flask_sermos.py +0 -156
  27. pypeline/generators.py +0 -196
  28. pypeline/logging_config.py +0 -171
  29. pypeline/pipeline/__init__.py +0 -0
  30. pypeline/pipeline/chained_task.py +0 -70
  31. pypeline/pipeline/generator.py +0 -254
  32. pypeline/sermos_yaml.py +0 -442
  33. pypeline/utils/graph_utils.py +0 -144
  34. pypeline/utils/task_utils.py +0 -552
  35. scalable_pypeline-1.2.3.dist-info/METADATA +0 -163
  36. scalable_pypeline-1.2.3.dist-info/RECORD +0 -33
  37. scalable_pypeline-1.2.3.dist-info/entry_points.txt +0 -2
  38. tests/fixtures/s3_fixtures.py +0 -52
  39. {scalable_pypeline-1.2.3.dist-info → scalable_pypeline-2.0.1.dist-info}/LICENSE +0 -0
  40. {scalable_pypeline-1.2.3.dist-info → scalable_pypeline-2.0.1.dist-info}/WHEEL +0 -0
  41. {scalable_pypeline-1.2.3.dist-info → scalable_pypeline-2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,326 +1,68 @@
1
- """ General utilities used frequently in configuration-related tasks.
2
-
3
- More specifically, these are methods that help interact with Pipeline and
4
- Schedule configurations that originate from your `sermos.yaml` file. These
5
- utility functions make it easy to switch between `local` and `cloud` modes
6
- based on the value of `DEFAULT_BASE_URL` in your environment.
7
-
8
- - If the base url is `local`, then all config tasks will read directly from
9
- your local `sermos.yaml` file. Update operations will *not* do anything (that
10
- is, your sermos.yaml file will not be updated).
11
-
12
- - If the base url is anything other than `local`, this will assume a cloud
13
- api url was provided (if None is set in environment, Sermos will default to
14
- the Sermos Cloud base API assuming this is a Sermos Cloud deployment). You can
15
- provide your own cloud API endpoints if desired, look to documentation for best
16
- practices.
17
-
18
- TODO Need to remove the dependency on Redis and make caching behavior optional.
19
- """
20
- import os
21
1
  import logging
22
- import json
23
- from typing import Union, Any
24
- from urllib.parse import urljoin
25
- import requests
26
- from rhodb.redis_conf import RedisConnector
27
- from pypeline.constants import DEFAULT_BASE_URL, PIPELINE_CONFIG_CACHE_KEY, \
28
- SCHEDULE_CONFIG_CACHE_KEY, CONFIG_REFRESH_RATE, USING_SERMOS_CLOUD, \
29
- LOCAL_DEPLOYMENT_VALUE, DEFAULT_CONFIG_RETRIEVAL_PAGE_SIZE
30
- from pypeline.sermos_yaml import load_sermos_config
31
- from pypeline.schedule_config_schema import BaseScheduleSchema
32
-
33
- logger = logging.getLogger(__name__)
34
- redis_conn = RedisConnector().get_connection()
35
-
36
-
37
- def get_access_key(access_key: Union[str, None] = None,
38
- env_var_name: str = 'SERMOS_ACCESS_KEY'):
39
- """ Simple helper to get admin server access key in a standard fashion. If
40
- one is provided, return it back. If not, look in environment for
41
- `env_var_name`. If that doesn't exist, raise useful error.
42
-
43
- If this is a local deployment, no access key is required/relevant,
44
- so simply return 'local'
45
- """
46
- if access_key is not None:
47
- return access_key
48
-
49
- if not USING_SERMOS_CLOUD:
50
- return LOCAL_DEPLOYMENT_VALUE # e.g. 'local'
51
-
52
- try:
53
- return os.environ[env_var_name]
54
- except KeyError:
55
- raise KeyError(
56
- f"{env_var_name} not found in this environment. Find a valid "
57
- "access key in your Sermos Cloud administration console.")
58
-
59
-
60
- # TODO cast to UUID?
61
- def get_deployment_id(deployment_id: Union[str, None] = None,
62
- env_var_name: str = 'SERMOS_DEPLOYMENT_ID'):
63
- """ Simple helper to get the deployment id in a standard fashion. Look in
64
- the environment for `env_var_name`. If that doesn't exist, raise useful
65
- error.
66
-
67
- If this is a local deployment, no deployment id is required/relevant,
68
- so this will simply return 'local' in the event the DEFAULT_BASE_URL is
69
- set to the LOCAL_DEPLOYMENT_VALUE ('local' by default) in the environment.
70
- """
71
- if deployment_id is not None:
72
- return deployment_id
73
-
74
- if not USING_SERMOS_CLOUD:
75
- return LOCAL_DEPLOYMENT_VALUE # e.g. 'local'
76
-
77
- try:
78
- return os.environ[env_var_name]
79
- except KeyError:
80
- raise KeyError(
81
- f"{env_var_name} not found in this environment. Note: this is "
82
- "required when running a Celery worker as `beat`. Find this ID "
83
- "in your administration console. For local development, this can "
84
- "be any arbitrary string.")
85
-
86
-
87
- def load_json_config_from_redis(key: str) -> Any:
88
- """ Load a json key from redis. Special carve out for keys explicitly set
89
- to "none".
90
- """
91
- val = redis_conn.get(key)
92
- if val is None or val.decode('utf-8').lower() == 'none':
93
- return None
94
- return json.loads(val)
95
-
96
-
97
- def set_json_config_to_redis(key: str,
98
- data: Union[dict, None],
99
- refresh_rate: int = CONFIG_REFRESH_RATE):
100
- """ For Admin API actions (e.g. schedules/pipelines), deployments cache
101
- results. The standard method for doing this is through a refresh key, which
102
- is set in redis to expire after the CONFIG_REFRESH_RATE. This will set
103
- the cached key.
104
-
105
- Rationale for manually setting a "None" key instead of simply skipping
106
- is to protect against case of a spammed config request for an unknown
107
- pipeline, for example. This will still limit our requests to Sermos Cloud
108
- based on the refresh rate even in that scenario.
109
- """
110
- if data is None:
111
- data = 'None'
112
- else:
113
- data = json.dumps(data)
114
-
115
- redis_conn.setex(key, refresh_rate, data)
116
-
117
-
118
- def _generate_api_url(endpoint: str = ''):
119
- """ Provide a normalized url based on the base url and endpoint and add in
120
- the deployment_id to the url, which is required for all default
121
- pipeline/schedule endpoints if using Sermos Cloud.
122
-
123
- The Sermos Cloud API spec bases everything on the notion of `deployments`,
124
- so if you are rolling your own 'non-local' API, you will need to mock this
125
- concept in order to use the built in helper functions for retrieving
126
- pipelines and schedules from an API source.
127
- """
128
- deployment_id = get_deployment_id() # From env if None
129
- return urljoin(DEFAULT_BASE_URL, f'deployments/{deployment_id}/{endpoint}')
130
-
2
+ from typing import Union
131
3
 
132
- def _retrieve_and_cache_config(key: str,
133
- admin_api_endpoint: str,
134
- access_key: str,
135
- refresh_rate: int = CONFIG_REFRESH_RATE) -> Any:
136
- """ Attempt to load a configuration (pipeline/schedule) from cache If not available,
137
- retrieve API response from Sermos Config Server and cache the response for
138
- CONFIG_REFRESH_RATE seconds in local Redis.
139
- """
140
- conf = load_json_config_from_redis(key)
141
- if conf is not None:
142
- return conf
143
-
144
- # Ask Sermos Cloud (Note: Sermos Cloud's API expects `apikey`)
145
- headers = {
146
- 'apikey': access_key,
147
- }
148
-
149
- params = {
150
- 'page_size': DEFAULT_CONFIG_RETRIEVAL_PAGE_SIZE,
151
- 'page': 1
152
- }
153
-
154
- r = requests.get(admin_api_endpoint, headers=headers, verify=True,
155
- params=params)
4
+ from pypeline.constants import WORKER_NAME
5
+ from pypeline.pypeline_yaml import load_pypeline_config
156
6
 
157
- data = None
158
- if r.status_code == 200:
159
- data = r.json()
160
- else:
161
- logger.warning(f"Non-200 response retrieving {admin_api_endpoint}: "
162
- f"{r.status_code}, {r.reason}")
163
-
164
- # There's a chance we need to request ALL schedule configs from sermos cloud
165
- # for the scheduled tasks. Lets loop and grab all of them.
166
- while key == SCHEDULE_CONFIG_CACHE_KEY and \
167
- len(data['data']['results']) < data['data']['count']:
168
- params['page'] += 1
169
- r = requests.get(admin_api_endpoint, headers=headers, verify=True,
170
- params=params)
171
- if r.status_code == 200:
172
- paginated_data = r.json()
173
- data['data']['results'] = data['data']['results'] + \
174
- paginated_data['data']['results']
175
- else:
176
- logger.warning(f"Non-200 response retrieving {admin_api_endpoint}: "
177
- f"{r.status_code}, {r.reason}")
178
- break
179
-
180
- # Cache result
181
- if data is not None:
182
- set_json_config_to_redis(key, data, refresh_rate)
183
-
184
- return data
7
+ logger = logging.getLogger(__name__)
185
8
 
186
9
 
187
10
  def retrieve_latest_pipeline_config(
188
- pipeline_id: Union[str, None] = None,
189
- access_key: Union[str, None] = None,
190
- refresh_rate: int = CONFIG_REFRESH_RATE) -> Union[dict, list]:
191
- """ Retrieve the 'latest' pipeline configuration.
192
-
193
- Sermos can be deployed in 'local' mode by setting DEFAULT_BASE_URL=local
194
- in your environment. In this case, Sermos will retrieve the latest
195
- configuration from the local filesystem, specifically looking inside the
196
- sermos.yaml file.
197
-
198
- If the DEFAULT_BASE_URL is anything else, this will assume that it is a
199
- valid API base url and make a request. The request will be formatted to
200
- match what Sermos Cloud expects for seamless Sermos Cloud deployments.
201
- However, you can provide any base url and stand up your own API if desired!
202
-
203
- This utilizes redis (required for Sermos-based pipelines/scheduled tasks)
204
- to cache the result for a predetermined amount of time before requesting an
205
- update. This is because pipelines/tasks can be invoked rapidly but do not
206
- change frequently.
207
- """
208
- # If this is a LOCAL deployment, look to sermos.yaml directly
209
- if not USING_SERMOS_CLOUD:
210
- sermos_config = load_sermos_config()
211
- if 'pipelines' in sermos_config:
212
- pipelines = []
213
- found_pipeline = None
214
- for p_id, config in sermos_config['pipelines'].items():
215
- config['sermosPipelineId'] = p_id
216
- if pipeline_id == p_id:
217
- found_pipeline = config
218
- break
219
- pipelines.append(config)
220
-
221
- if pipeline_id:
222
- if found_pipeline:
223
- return found_pipeline
224
- raise ValueError(f'Invalid pipeline {pipeline_id}')
225
-
226
- return pipelines
227
- return None
228
-
229
- # If this is a CLOUD deployment, generate a valid API url and ask the API
230
- # service for pipeline configuration. If this deployment is set up to
231
- # cache results, do so.
232
- cache_key = PIPELINE_CONFIG_CACHE_KEY.format(pipeline_id)
233
- access_key = get_access_key(access_key) # From env if None
11
+ pipeline_id: Union[str, None] = None
12
+ ) -> Union[dict, list]:
13
+ """Retrieve the 'latest' pipeline configuration for a given pipeline."""
14
+ pypeline_config = load_pypeline_config()
15
+ if "pipelines" in pypeline_config:
16
+ pipelines = []
17
+ found_pipeline = None
18
+ for p_id, config in pypeline_config["pipelines"].items():
19
+ if pipeline_id == p_id:
20
+ found_pipeline = config
21
+ break
22
+ pipelines.append(config)
234
23
 
235
- # Generate pipeline specific API endpoint. If pipeline_id
236
- # is None, then we're asking for 'all' pipelines.
237
- api_url = _generate_api_url('pipelines')
238
- if pipeline_id is not None:
239
- api_url = urljoin(api_url + '/', pipeline_id) # Add pipeline ID
240
-
241
- # Retrieve (and cache) result - this will be the exact result from the
242
- # API response.
243
- data = _retrieve_and_cache_config(cache_key, api_url, access_key,
244
- refresh_rate)
245
- if data:
246
24
  if pipeline_id:
247
- return data['data']
248
- return data['data']['results']
249
- return None
25
+ if found_pipeline:
26
+ return found_pipeline
27
+ raise ValueError(f"Invalid pipeline {pipeline_id}")
250
28
 
29
+ return pipelines
30
+ return None
251
31
 
252
- def retrieve_latest_schedule_config(access_key: Union[str, None] = None,
253
- refresh_rate: int = CONFIG_REFRESH_RATE):
254
- """ Retrieve the 'latest' scheduled tasks configuration.
255
32
 
256
- Sermos can be deployed in 'local' mode by setting DEFAULT_BASE_URL=local
257
- in your environment. In this case, Sermos will retrieve the latest configuration
258
- from the local filesystem, specifically looking inside the sermos.yaml file.
33
+ def retrieve_latest_schedule_config():
34
+ """Retrieve the 'latest' scheduled tasks configuration."""
35
+ pypeline_config = load_pypeline_config()
36
+ if "scheduledTasks" in pypeline_config:
37
+ tasks = []
38
+ for task_id, config in pypeline_config["scheduledTasks"].items():
39
+ tasks.append(config)
40
+ return tasks
41
+ return None
259
42
 
260
- If the DEFAULT_BASE_URL is anything else, this will assume that it is a valid
261
- API base url and make a request. The request will be formatted to match what
262
- Sermos Cloud expects for seamless Sermos Cloud deployments. However, you can
263
- provide any base url and stand up your own API if desired!
264
43
 
265
- This utilizes redis (required for Sermos-based pipelines/scheduled tasks) to
266
- cache the result for a predetermined amount of time before requesting an
267
- update. This is because pipelines/tasks can be invoked rapidly but do not
268
- change frequently.
44
+ def get_service_config_for_worker(
45
+ pypeline_config: dict, worker_name: str = None
46
+ ) -> Union[dict, None]:
47
+ """For the current WORKER_NAME (which must be present in the environment
48
+ of this worker instance for a valid deployment), return the worker's
49
+ serviceConfig object.
269
50
  """
270
- if not USING_SERMOS_CLOUD:
271
- sermos_config = load_sermos_config()
272
- if 'scheduledTasks' in sermos_config:
273
- tasks = []
274
- for task_id, config in sermos_config['scheduledTasks'].items():
275
- config['sermosScheduledTasksId'] = task_id
276
- tasks.append(config)
277
- return tasks
51
+ if pypeline_config is None:
52
+ raise ValueError("Pypeline config was not provided")
53
+ if worker_name is None:
54
+ worker_name = WORKER_NAME
55
+ if worker_name is None:
278
56
  return None
279
57
 
280
- cache_key = SCHEDULE_CONFIG_CACHE_KEY
281
- access_key = get_access_key(access_key) # From env if None
282
-
283
- api_url = _generate_api_url('scheduled_tasks')
284
-
285
- data = _retrieve_and_cache_config(cache_key, api_url, access_key,
286
- refresh_rate)
287
-
288
- schedules = []
289
- for schedule in data['data']['results']:
290
- ScheduleSchema = \
291
- BaseScheduleSchema.get_by_version(schedule['schemaVersion'])
292
- schema = ScheduleSchema()
293
- _schedule = schema.load(schedule)
294
- _schedule['id'] = schedule['id']
295
- schedules.append(_schedule)
296
-
297
- return schedules
298
-
299
-
300
- def update_schedule_config(new_schedule_config: dict,
301
- access_key: Union[str, None] = None,
302
- schedule_config_endpoint: Union[str, None] = None):
303
- """ Tell Sermos to update a deployment's schedule with new version.
304
- """
305
-
306
- # Don't send status to sermos-cloud if we're running in local mode
307
- if not USING_SERMOS_CLOUD:
308
- return True
309
-
310
- access_key = get_access_key(access_key) # From env if None
311
- api_url = _generate_api_url('scheduled_tasks')
312
-
313
- # Ask Sermos Cloud (Note: Sermos Cloud's API expects `apikey`)
314
- headers = {'apikey': access_key}
315
-
316
- for scheduled_task in new_schedule_config['schedules']:
317
- copy_task = dict(scheduled_task)
318
- task_id = copy_task.pop('id')
319
- url = f"{api_url}/{task_id}"
320
- r = requests.put(url, json=copy_task, headers=headers, verify=True)
321
- if r.status_code != 200:
322
- logger.error("Unable to update schedule task in sermos cloud")
323
- logger.error(r.json())
324
- return False
325
-
326
- return True
58
+ service_config = pypeline_config.get("serviceConfig", [])
59
+ for service in service_config:
60
+ if service["name"] == worker_name:
61
+ return service
62
+
63
+ raise ValueError(
64
+ "Could not find a service config for worker "
65
+ f"`{worker_name}`. Make sure you have added the service in"
66
+ f" your pypeline.yaml with `name: {worker_name}` and "
67
+ "`type: celery-worker`."
68
+ )
@@ -5,32 +5,32 @@ import re
5
5
  import logging
6
6
  import importlib
7
7
  from typing import Callable
8
- from pypeline.constants import SERMOS_ACCESS_KEY, SERMOS_CLIENT_PKG_NAME
8
+ from pypeline.constants import API_ACCESS_KEY, PYPELINE_CLIENT_PKG_NAME
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
12
 
13
- class SermosModuleLoader(object):
14
- """ Helper class to load modules / classes / methods based on a path string.
15
- """
13
+ class PypelineModuleLoader(object):
14
+ """Helper class to load modules / classes / methods based on a path string."""
15
+
16
16
  def get_module(self, resource_dot_path: str):
17
- """ Retrieve the module based on a 'resource dot path'.
18
- e.g. package.subdir.feature_file.MyCallable
17
+ """Retrieve the module based on a 'resource dot path'.
18
+ e.g. package.subdir.feature_file.MyCallable
19
19
  """
20
- module_path = '.'.join(resource_dot_path.split('.')[:-1])
20
+ module_path = ".".join(resource_dot_path.split(".")[:-1])
21
21
  module = importlib.import_module(module_path)
22
22
  return module
23
23
 
24
24
  def get_callable_name(self, resource_dot_path: str) -> str:
25
- """ Retrieve the callable based on config string.
26
- e.g. package.subdir.feature_file.MyCallable
25
+ """Retrieve the callable based on config string.
26
+ e.g. package.subdir.feature_file.MyCallable
27
27
  """
28
- callable_name = resource_dot_path.split('.')[-1]
28
+ callable_name = resource_dot_path.split(".")[-1]
29
29
  return callable_name
30
30
 
31
31
  def get_callable(self, resource_dot_path: str) -> Callable:
32
- """ Retrieve the actual handler class based on config string.
33
- e.g. package.subdir.feature_file.MyCallable
32
+ """Retrieve the actual handler class based on config string.
33
+ e.g. package.subdir.feature_file.MyCallable
34
34
  """
35
35
  module = self.get_module(resource_dot_path)
36
36
  callable_name = self.get_callable_name(resource_dot_path)
@@ -38,82 +38,46 @@ class SermosModuleLoader(object):
38
38
 
39
39
 
40
40
  def normalized_pkg_name(pkg_name: str, dashed: bool = False):
41
- """ We maintain consistency by always specifying the package name as
42
- the "dashed version".
43
-
44
- Python/setuptools will replace "_" with "-" but resource_filename()
45
- expects the exact directory name, essentially. In order to keep it
46
- simple upstream and *always* provide package name as the dashed
47
- version, we do replacement here to 'normalize' both versions to
48
- whichever convention you need at the time.
49
-
50
- if `dashed`:
51
- my-package-name --> my-package-name
52
- my_package_name --> my-package-name
53
-
54
- else:
55
- my-package-name --> my_package_name
56
- my_package_name --> my_package_name
41
+ """We maintain consistency by always specifying the package name as
42
+ the "dashed version".
43
+
44
+ Python/setuptools will replace "_" with "-" but resource_filename()
45
+ expects the exact directory name, essentially. In order to keep it
46
+ simple upstream and *always* provide package name as the dashed
47
+ version, we do replacement here to 'normalize' both versions to
48
+ whichever convention you need at the time.
49
+
50
+ if `dashed`:
51
+ my-package-name --> my-package-name
52
+ my_package_name --> my-package-name
53
+
54
+ else:
55
+ my-package-name --> my_package_name
56
+ my_package_name --> my_package_name
57
57
  """
58
58
  if dashed:
59
- return str(pkg_name).replace('_', '-')
60
- return str(pkg_name).replace('-', '_')
61
-
62
-
63
- def get_client_pkg_name(pkg_name: str = None):
64
- """ Verify the package name provided and get from environment if None.
65
-
66
- Raise if neither provided nor found.
67
-
68
- Arguments:
69
- pkg_name (optional): Directory name for your Python
70
- package. e.g. my_package_name . If none provided, will check
71
- environment for `SERMOS_CLIENT_PKG_NAME`. If not found,
72
- will exit.
73
- """
74
- pkg_name = pkg_name if pkg_name else SERMOS_CLIENT_PKG_NAME
75
- if pkg_name is None:
76
- msg = "Unable to find `pkg-name` in CLI arguments nor in "\
77
- "environment under `{}`".format('SERMOS_CLIENT_PKG_NAME')
78
- logger.error(msg)
79
- raise ValueError(msg)
80
- return pkg_name
59
+ return str(pkg_name).replace("_", "-")
60
+ return str(pkg_name).replace("-", "_")
81
61
 
82
62
 
83
63
  def match_prefix(string: str, prefix_p: str) -> bool:
84
- """ For given string, determine whether it begins with provided prefix_p.
85
- """
86
- pattern = re.compile('^(' + prefix_p + ').*')
64
+ """For given string, determine whether it begins with provided prefix_p."""
65
+ pattern = re.compile("^(" + prefix_p + ").*")
87
66
  if pattern.match(string):
88
67
  return True
89
68
  return False
90
69
 
91
70
 
92
71
  def match_suffix(string: str, suffix_p: str) -> bool:
93
- """ For given string, determine whether it ends with provided suffix_p.
94
- """
95
- pattern = re.compile('.*(' + suffix_p + ')$')
72
+ """For given string, determine whether it ends with provided suffix_p."""
73
+ pattern = re.compile(".*(" + suffix_p + ")$")
96
74
  if pattern.match(string):
97
75
  return True
98
76
  return False
99
77
 
100
78
 
101
79
  def match_prefix_suffix(string: str, prefix_p: str, suffix_p: str) -> bool:
102
- """ For given string, determine whether it starts w/ prefix & ends w/ suffix
103
- """
80
+ """For given string, determine whether it starts w/ prefix & ends w/ suffix"""
104
81
  if match_prefix(string, prefix_p) and match_suffix(string, suffix_p):
105
82
  return True
106
83
  return False
107
-
108
-
109
- def find_from_environment(prefix_p: str, suffix_p: str) -> list:
110
- """ Find all envirionment variables that match prefix and suffix.
111
-
112
- Can provide any regex compatible string as values.
113
- """
114
- matching_vars = []
115
- environment_vars = os.environ
116
- for var in environment_vars:
117
- if match_prefix_suffix(var, prefix_p, suffix_p):
118
- matching_vars.append(var)
119
- return matching_vars