data_pipeline_api 0.8.0__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,27 @@
1
+ __all__ = [
2
+ "initialise",
3
+ "link_read",
4
+ "link_write",
5
+ "finalise",
6
+ "raise_issue_by_data_product",
7
+ "raise_issue_by_index",
8
+ "raise_issue_with_config",
9
+ "raise_issue_by_type",
10
+ "raise_issue_by_existing_data_product",
11
+ "raise_issue_with_submission_script",
12
+ "raise_issue_with_github_repo",
13
+ "get_handle_index_from_path",
14
+ ]
15
+
16
+ from .fdp_utils import get_handle_index_from_path
17
+ from .link import link_read, link_write
18
+ from .pipeline import finalise, initialise
19
+ from .raise_issue import (
20
+ raise_issue_by_data_product,
21
+ raise_issue_by_existing_data_product,
22
+ raise_issue_by_index,
23
+ raise_issue_by_type,
24
+ raise_issue_with_config,
25
+ raise_issue_with_github_repo,
26
+ raise_issue_with_submission_script,
27
+ )
@@ -0,0 +1,43 @@
1
+ import os
2
+ import platform
3
+ import sys
4
+
5
+ sys.path.append(str(os.getcwd()))
6
+ import data_pipeline_api as pipeline # noqa: E402
7
+ import simpleModel as simpleModel # noqa: E402
8
+
9
+ token = str(os.environ.get("FDP_LOCAL_TOKEN"))
10
+ script = os.path.join(str(os.environ.get("FDP_CONFIG_DIR")), "script.sh")
11
+ if platform.system() == "Windows":
12
+ script = os.path.join(str(os.environ.get("FDP_CONFIG_DIR")), "script.bat")
13
+ config = os.path.join(str(os.environ.get("FDP_CONFIG_DIR")), "config.yaml")
14
+ handle = pipeline.initialise(token, config, script)
15
+
16
+ initial_parameters = pipeline.link_read(handle, "SEIRS_model/parameters")
17
+ model_output = pipeline.link_write(
18
+ handle, "SEIRS_model/results/model_output/python"
19
+ )
20
+ model_plot = pipeline.link_write(handle, "SEIRS_model/results/figure/python")
21
+
22
+ # Model Code
23
+ states = {"s": 0.999, "e": 0.001, "i": 0, "r": 0}
24
+
25
+ init_params = simpleModel.readInitialParameters(initial_parameters)
26
+
27
+ alpha = init_params["alpha"]
28
+ beta = init_params["beta"]
29
+ inv_gamma = init_params["inv_gamma"]
30
+ inv_omega = init_params["inv_omega"]
31
+ inv_mu = init_params["inv_mu"]
32
+ inv_sigma = init_params["inv_sigma"]
33
+ R0 = init_params["R0"]
34
+
35
+ sm = simpleModel.SEIRS_Model(
36
+ states, 1000, 5, alpha, beta, inv_gamma, inv_omega, inv_mu, inv_sigma
37
+ )
38
+
39
+ simpleModel.SEIRS_Plot(sm, model_plot)
40
+
41
+ simpleModel.write_model_to_csv(sm, model_output)
42
+
43
+ pipeline.finalise(token, handle)
@@ -0,0 +1,31 @@
1
+ run_metadata:
2
+ default_input_namespace: rfield
3
+ description: SEIRS Model python
4
+ script: python3 data_pipeline_api/ext/SEIRSModelRun.py
5
+ remote_repo: https://github.com/FAIRDataPipeline/pyDataPipeline.git
6
+
7
+ register:
8
+ - namespace: PSU
9
+ full_name: Pennsylvania State University
10
+ website: https://ror.org/04p491231
11
+
12
+ - external_object: SEIRS_model/parameters
13
+ namespace_name: PSU
14
+ root: https://raw.githubusercontent.com/
15
+ path: FAIRDataPipeline/pyDataPipeline/dev/data_pipeline_api/ext/static_params_SEIRS.csv
16
+ title: Static parameters of the model
17
+ description: Static parameters of the model
18
+ identifier: https://doi.org/10.1038/s41592-020-0856-2
19
+ file_type: csv
20
+ release_date: 2021-09-20T12:00
21
+ version: "1.0.0"
22
+ primary: True
23
+
24
+ write:
25
+ - data_product: SEIRS_model/results/model_output/python
26
+ description: SEIRS model results
27
+ file_type: csv
28
+
29
+ - data_product: SEIRS_model/results/figure/python
30
+ description: SEIRS output plot
31
+ file_type: png
@@ -0,0 +1,31 @@
1
+ run_metadata:
2
+ default_input_namespace: rfield
3
+ description: SEIRS Model python
4
+ script: python data_pipeline_api/ext/SEIRSModelRun.py
5
+ remote_repo: https://github.com/FAIRDataPipeline/pyDataPipeline.git
6
+
7
+ register:
8
+ - namespace: PSU
9
+ full_name: Pennsylvania State University
10
+ website: https://ror.org/04p491231
11
+
12
+ - external_object: SEIRS_model/parameters
13
+ namespace_name: PSU
14
+ root: https://raw.githubusercontent.com/
15
+ path: FAIRDataPipeline/pyDataPipeline/dev/data_pipeline_api/ext/static_params_SEIRS.csv
16
+ title: Static parameters of the model
17
+ description: Static parameters of the model
18
+ identifier: https://doi.org/10.1038/s41592-020-0856-2
19
+ file_type: csv
20
+ release_date: 2021-09-20T12:00
21
+ version: "1.0.0"
22
+ primary: True
23
+
24
+ write:
25
+ - data_product: SEIRS_model/results/model_output/python
26
+ description: SEIRS model results
27
+ file_type: csv
28
+
29
+ - data_product: SEIRS_model/results/figure/python
30
+ description: SEIRS output plot
31
+ file_type: png
File without changes
@@ -0,0 +1,8 @@
1
+ "param","value"
2
+ "alpha",0
3
+ "beta",0.21
4
+ "inv_gamma",14
5
+ "inv_omega",1
6
+ "inv_mu",76
7
+ "inv_sigma",7
8
+ "R0",3
@@ -0,0 +1,517 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import os
5
+ import random
6
+ import uuid
7
+ from datetime import datetime
8
+ from typing import Any, Optional
9
+
10
+ from urllib.parse import urlsplit
11
+
12
+ import requests
13
+ import yaml
14
+
15
+ FILE_PREFIX = "file://"
16
+ SERVER_RESPONSE_STR = "Server responded with: "
17
+
18
+ def get_first_entry(entries: list) -> dict:
19
+ """
20
+ get_first_entry helper function for get_entry that return first element
21
+
22
+ exception handling is done in the main code
23
+
24
+ Parameters
25
+ ----------
26
+ entries : list
27
+ [response list from api]
28
+
29
+ Returns
30
+ -------
31
+ dict
32
+ [dictionary output from api]
33
+ """
34
+ return entries[0]
35
+
36
+
37
+ def get_entry(
38
+ url: str,
39
+ endpoint: str,
40
+ query: dict,
41
+ token: str = None,
42
+ api_version: str = "1.0.0",
43
+ ) -> list:
44
+ """
45
+ Internal function to retreive and item from the registry using a query
46
+ Args:
47
+ | url: str of the registry url
48
+ | endpoint: endpoint (table)
49
+ | query: dict forming a query
50
+ | token: (optional) str of the registry token
51
+ Returns:
52
+ | dict: responce from registry
53
+ """
54
+ headers = get_headers(token=token, api_version=api_version)
55
+
56
+ # Remove api address from query
57
+ for key in query:
58
+ if isinstance(query[key], str):
59
+ if url in query[key]:
60
+ query[key] = extract_id(query[key])
61
+ elif isinstance(query[key], dict):
62
+ for _key in query[key]:
63
+ if url in query[key][_key]:
64
+ query[key][_key] = extract_id(query[key][_key])
65
+ elif isinstance(query[key], list):
66
+ for i in range(len(query[key])):
67
+ if url in query[key][i]:
68
+ query[key][i] = extract_id(query[key][i])
69
+
70
+ if url[-1] != "/":
71
+ url += "/"
72
+ url += endpoint + "/?"
73
+ _query = [f"{k}={v}" for k, v in query.items()]
74
+ url += "&".join(_query)
75
+ response = requests.get(url, headers=headers)
76
+ if response.status_code != 200:
77
+ raise ValueError(
78
+ SERVER_RESPONSE_STR
79
+ + str(response.status_code)
80
+ + " Query = "
81
+ + url
82
+ )
83
+ return response.json()["results"]
84
+
85
+
86
+ def get_entity(
87
+ url: str,
88
+ endpoint: str,
89
+ id: int,
90
+ token: str = None,
91
+ api_version: str = "1.0.0",
92
+ ) -> dict:
93
+ """
94
+ Internal function to get an item from the registry using it's id
95
+ Args:
96
+ | url: str of the registry url
97
+ | enpoint: endpoint (table)
98
+ | id: id of the item
99
+ | token: (optional) str of the registry token
100
+ Returns:
101
+ | dict: responce from registry
102
+ """
103
+ headers = get_headers(token=token, api_version=api_version)
104
+
105
+ if url[-1] != "/":
106
+ url += "/"
107
+ url += endpoint + "/" + str(id)
108
+ response = requests.get(url, headers=headers)
109
+ if response.status_code != 200:
110
+ raise ValueError(
111
+ SERVER_RESPONSE_STR
112
+ + str(response.status_code)
113
+ + " Query = "
114
+ + url
115
+ )
116
+ return response.json()
117
+
118
+
119
+ def extract_id(url: str) -> str:
120
+ """
121
+ Internal function to return the id from an api url
122
+ Args:
123
+ | url: str of the api url
124
+ Returns:
125
+ | str: id derrived from the url
126
+ """
127
+
128
+ split_url_path = urlsplit(url).path.split("/")
129
+ if not split_url_path:
130
+ raise IndexError(f"Unable to extract ID from registry URL: {url}")
131
+ return [s for s in split_url_path if s != ""][-1]
132
+
133
+
134
+ def post_entry(
135
+ url: str, endpoint: str, data: dict, token: str, api_version: str = "1.0.0"
136
+ ) -> dict:
137
+ """
138
+ Internal function to post and entry on the registry
139
+ Args:
140
+ | url: str of the registry url
141
+ | enpoint: str of the endpoint (table)
142
+ | data: a dictionary containing the data to be posted
143
+ | token: str of the registry token
144
+ Returns:
145
+ | dict: responce from registry
146
+ """
147
+ headers = get_headers(
148
+ request_type="post", token=token, api_version=api_version
149
+ )
150
+
151
+ if url[-1] != "/":
152
+ url += "/"
153
+ _url = url + endpoint + "/"
154
+ _data = json.dumps(data)
155
+
156
+ response = requests.post(_url, _data, headers=headers)
157
+
158
+ if response.status_code == 409:
159
+ logging.info("Entry Exists: Attempting to return Existing Entry")
160
+ existing_entry = get_entry(url, endpoint, data)
161
+ if not existing_entry:
162
+ raise ValueError("Could not return existing Entry")
163
+ return existing_entry[0]
164
+
165
+ if response.status_code != 201:
166
+ raise ValueError(SERVER_RESPONSE_STR + str(response.status_code))
167
+
168
+ return response.json()
169
+
170
+
171
+ def patch_entry(
172
+ url: str, data: dict, token: str, api_version: str = "1.0.0"
173
+ ) -> dict:
174
+ """
175
+ Internal function to patch and entry on the registry
176
+ Args:
177
+ | url: str of the url of what to be patched
178
+ | data: a dictionary containing the data to be patched
179
+ | token: str of the registry token
180
+ Returns:
181
+ | dict: responce from registry
182
+ """
183
+ headers = get_headers(
184
+ request_type="post", token=token, api_version=api_version
185
+ )
186
+
187
+ data_json = json.dumps(data)
188
+
189
+ response = requests.patch(url, data_json, headers=headers)
190
+ if response.status_code != 200:
191
+ raise ValueError(SERVER_RESPONSE_STR + str(response.status_code))
192
+
193
+ return response.json()
194
+
195
+
196
+ def get_headers(
197
+ request_type: str = "get", token: str = None, api_version: str = "1.0.0"
198
+ ) -> dict:
199
+ """
200
+ Internal function to return headers to be added to a request
201
+ Args:
202
+ | request_type: (optional) type of request e.g. 'post' or 'get' defaults to 'get'
203
+ | token: (optional) token, if a token is supplied this will be added to the headers
204
+ | api_version: (optional) the version of the data registy to interact with, defaults to '1.0.0'
205
+ Returns:
206
+ | dict: a dictionary of appropriate headers to be added to a request
207
+ """
208
+ headers = {"Accept": "application/json; version=" + api_version}
209
+ if token:
210
+ headers["Authorization"] = "token " + token
211
+ if request_type == "post":
212
+ headers["Content-Type"] = "application/json"
213
+ return headers
214
+
215
+
216
+ def post_storage_root(
217
+ url: str, data: dict, token: str, api_version: str = "1.0.0"
218
+ ) -> dict:
219
+ """
220
+ Internal function to post a storage root to the registry
221
+ the function first adds file:// if the root is local
222
+ Args:
223
+ | url: str the url for the storage root e.g. https://github.com/
224
+ Returns:
225
+ | dict: repsonse from the local registy
226
+ """
227
+ if "local" in data and data["local"]:
228
+ data["root"] = FILE_PREFIX + data["root"]
229
+ if data["root"][-1] != os.sep:
230
+ data["root"] = data["root"] + os.sep
231
+ elif data["root"][-1] != "/":
232
+ data["root"] = data["root"] + "/"
233
+ return post_entry(url, "storage_root", data, token, api_version)
234
+
235
+ def post_file_type(
236
+ url: str, data: dict, token: str, api_version: str = "1.0.0"
237
+ ) -> dict:
238
+ """
239
+ Internal wrapper function to return check if a file_type already exists and return it.
240
+ """
241
+ if "extension" not in data:
242
+ if not data["extension"]:
243
+ raise ValueError("error file_type name not specified")
244
+ file_type_exists = get_entry(url= url,
245
+ endpoint= "file_type",
246
+ query= {"extension": data["extension"]},
247
+ api_version= api_version)
248
+ if file_type_exists : return file_type_exists[0]
249
+ return post_entry(url= url,
250
+ endpoint= "file_type",
251
+ data= data,
252
+ token= token,
253
+ api_version= api_version)
254
+
255
+ def remove_local_from_root(root: str) -> str:
256
+ """
257
+ Internal function to remove prepending file:// from a given root
258
+ Args:
259
+ | root: the root
260
+ Returns:
261
+ | str: the root without file://
262
+ """
263
+ if FILE_PREFIX in root:
264
+ root = root.replace(FILE_PREFIX, "")
265
+
266
+ return root
267
+
268
+
269
+ def random_hash() -> str:
270
+ """
271
+ Internal function to generate a random unique hash
272
+
273
+ Returns:
274
+ | str: 40 character randomly generated hash.
275
+ """
276
+ seed = datetime.now().timestamp() * random.uniform(1, 1000000)
277
+ seed_encoded = str(seed).encode("utf-8")
278
+ hashed = hashlib.sha1(seed_encoded)
279
+
280
+ return hashed.hexdigest()
281
+
282
+
283
+ def get_file_hash(
284
+ path: str,
285
+ ) -> str:
286
+ """
287
+ Internal function to return a files sha1 hash
288
+ Args:
289
+ | path: str file path
290
+ Returns:
291
+ | str: sha1 hash
292
+ """
293
+ with open(path, "rb") as data:
294
+ _data = data.read()
295
+ hashed = hashlib.sha1(_data)
296
+
297
+ return hashed.hexdigest()
298
+
299
+
300
+ def read_token(token_path: str) -> str:
301
+ """
302
+ Internal function read a token from a given file
303
+ Args:
304
+ | token_path: path to token
305
+ Returns:
306
+ | str: token
307
+ """
308
+ with open(token_path) as token:
309
+ _token = token.readline().strip()
310
+ return _token
311
+
312
+
313
+ def get_token(token_path: str) -> str:
314
+ """
315
+ Internal function alias for read_token()
316
+ Args:
317
+ | token_path: path to token
318
+ Returns:
319
+ | str: token
320
+ """
321
+ return read_token(token_path)
322
+
323
+
324
+ def is_file(filename: str) -> bool:
325
+ """
326
+ Internal function to check whether a file exists
327
+ Args:
328
+ | filename: file to check
329
+ Returns:
330
+ | boolean: whether the file exists
331
+ """
332
+ return os.path.isfile(filename)
333
+
334
+
335
+ def is_yaml(filename: str) -> bool:
336
+ """
337
+ Internal function to check whether a file can be opened as a YAML file
338
+ ! warning returns True if the file can be coerced into yaml format
339
+ Args:
340
+ | filename: path to the yaml file
341
+ Returns:
342
+ | boolean: can the file be coerced into a yaml format?
343
+ """
344
+ try:
345
+ with open(filename, "r") as data:
346
+ yaml.safe_load(data)
347
+ except Exception as err:
348
+ print(f"{type(err).__name__} was raised: {err}")
349
+ return False
350
+ return True
351
+
352
+
353
+ def is_valid_yaml(filename: str) -> bool:
354
+ """
355
+ Internal function validate whether a file exists and can be coerced into a yaml format
356
+ Args:
357
+ | filename: path to the yaml file
358
+ Returns:
359
+ | boolean: does the file exist and can it be coerced into a yaml format
360
+ """
361
+ return is_file(filename) & is_yaml(filename)
362
+
363
+
364
+ def generate_uuid() -> str:
365
+ """
366
+ Internal function similar to random hash
367
+ Returns:
368
+ | str: a random unique identifier
369
+ """
370
+ return datetime.now().strftime("%Y%m-%d%H-%M%S-") + str(uuid.uuid4())
371
+
372
+
373
+ def get_handle_index_from_path(handle: dict, path: str) -> Optional[Any]:
374
+ """
375
+ Get an input or output handle index from a path
376
+ usually generated by link_read or link_write
377
+
378
+ Args:
379
+ | handle: the handle containing the index
380
+ | path: path as generated by link_read or link_write
381
+ """
382
+ tmp = None
383
+ if "output" in handle:
384
+ for output in handle["output"]:
385
+ if handle["output"][output]["path"] == path:
386
+ tmp = output
387
+ if "input" in handle:
388
+ for input in handle["input"]:
389
+ if handle["input"][input]["path"] == path:
390
+ tmp = input
391
+ return tmp
392
+
393
+
394
+ # flake8: noqa C901
395
+ def register_issues(token: str, handle: dict) -> dict: # sourcery no-metrics
396
+ """
397
+ Internal function, should only be called from finalise.
398
+ """
399
+
400
+ api_url = handle["yaml"]["run_metadata"]["local_data_registry_url"]
401
+ issues = handle["issues"]
402
+ groups = {handle["issues"][i]["group"] for i in handle["issues"]}
403
+ api_version = handle["yaml"]["run_metadata"]["api_version"]
404
+
405
+ for group in groups:
406
+ component_list = []
407
+ issue = None
408
+ severity = None
409
+ for i in issues:
410
+ if issues[i]["group"] == group:
411
+ issue_type = issues[i]["type"]
412
+ issue = issues[i]["issue"]
413
+ severity = issues[i]["severity"]
414
+ index = issues[i]["index"]
415
+ data_product = issues[i]["use_data_product"]
416
+ component = issues[i]["use_component"]
417
+ version = issues[i]["version"]
418
+ namespace = issues[i]["use_namespace"]
419
+
420
+ component_url = None
421
+ object_id = None
422
+ if issue_type == "config":
423
+ object_id = handle["model_config"]
424
+ elif issue_type == "github_repo":
425
+ object_id = handle["code_repo"]
426
+
427
+ elif issue_type == "submission_script":
428
+ object_id = handle["submission_script"]
429
+ if object_id:
430
+ component_url = get_entry(
431
+ url=api_url,
432
+ endpoint="object_component",
433
+ query={
434
+ "object": extract_id(object_id),
435
+ "whole_object": True,
436
+ },
437
+ api_version=api_version,
438
+ )[0]["url"]
439
+
440
+ if index:
441
+ if "output" in handle:
442
+ for ii in handle["output"]:
443
+ if handle["output"][ii] == index:
444
+ if (
445
+ "component_url"
446
+ in handle["output"][ii].keys()
447
+ ):
448
+ component_url = handle["output"][ii][
449
+ "component_url"
450
+ ]
451
+ else:
452
+ logging.warning("No Component Found")
453
+ if "input" in handle:
454
+ for ii in handle["input"]:
455
+ if handle["input"][ii] == index:
456
+ if (
457
+ "component_url"
458
+ in handle["input"][ii].keys()
459
+ ):
460
+ component_url = handle["input"][ii][
461
+ "component_url"
462
+ ]
463
+ else:
464
+ logging.warning("No Component Found")
465
+
466
+ if data_product:
467
+ current_namespace = get_entry(
468
+ url=api_url,
469
+ endpoint="namespace",
470
+ query={"name": namespace},
471
+ api_version=api_version,
472
+ )[0]["url"]
473
+
474
+ object_entry = get_entry(
475
+ url=api_url,
476
+ endpoint="data_product",
477
+ query={
478
+ "name": data_product,
479
+ "version": version,
480
+ "namespace": extract_id(current_namespace),
481
+ },
482
+ api_version=api_version,
483
+ )[0]["object"]
484
+ object_id = extract_id(object_entry)
485
+ if component:
486
+ component_url = get_entry(
487
+ url=api_url,
488
+ endpoint="object_component",
489
+ query={"name": component, "object": object_id},
490
+ api_version=api_version,
491
+ )
492
+ else:
493
+ component_obj = get_entry(
494
+ url=api_url,
495
+ endpoint="object_component",
496
+ query={"object": object_id, "whole_object": True},
497
+ api_version=api_version,
498
+ )
499
+ component_url = component_obj[0]["url"]
500
+
501
+ if component_url:
502
+ component_list.append(component_url)
503
+
504
+ # Register the issue:
505
+ logging.info("Registering issue: {}".format(group))
506
+ current_issue = post_entry(
507
+ url=api_url,
508
+ endpoint="issue",
509
+ data={
510
+ "severity": severity,
511
+ "description": issue,
512
+ "component_issues": component_list,
513
+ },
514
+ token=token,
515
+ api_version=api_version,
516
+ )
517
+ return current_issue