artefacts-cli 0.9.3__py3-none-any.whl → 0.9.5__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.
- artefacts/cli/__init__.py +31 -36
- artefacts/cli/app.py +98 -43
- artefacts/cli/config.py +150 -2
- artefacts/cli/helpers.py +12 -0
- artefacts/cli/locales/art.pot +97 -64
- artefacts/cli/locales/base.pot +97 -64
- artefacts/cli/locales/click.pot +2 -2
- artefacts/cli/logger.py +5 -1
- artefacts/cli/ros2.py +1 -1
- artefacts/cli/version.py +2 -2
- artefacts/copava/__init__.py +1 -1
- {artefacts_cli-0.9.3.dist-info → artefacts_cli-0.9.5.dist-info}/METADATA +5 -1
- {artefacts_cli-0.9.3.dist-info → artefacts_cli-0.9.5.dist-info}/RECORD +16 -16
- {artefacts_cli-0.9.3.dist-info → artefacts_cli-0.9.5.dist-info}/WHEEL +0 -0
- {artefacts_cli-0.9.3.dist-info → artefacts_cli-0.9.5.dist-info}/entry_points.txt +0 -0
- {artefacts_cli-0.9.3.dist-info → artefacts_cli-0.9.5.dist-info}/top_level.txt +0 -0
artefacts/cli/__init__.py
CHANGED
@@ -6,7 +6,6 @@ import glob
|
|
6
6
|
import json
|
7
7
|
import math
|
8
8
|
import os
|
9
|
-
import requests
|
10
9
|
|
11
10
|
from .config import APIConf
|
12
11
|
from .i18n import localise
|
@@ -33,7 +32,15 @@ except PackageNotFoundError:
|
|
33
32
|
__version__ = "0.0.0"
|
34
33
|
|
35
34
|
|
36
|
-
class
|
35
|
+
class ArtefactsAPIError(Exception):
|
36
|
+
"""
|
37
|
+
Tentative base error class for Artefacts API interactions
|
38
|
+
"""
|
39
|
+
|
40
|
+
pass
|
41
|
+
|
42
|
+
|
43
|
+
class AuthenticationError(ArtefactsAPIError):
|
37
44
|
"""Raised when artefacts authentication failed"""
|
38
45
|
|
39
46
|
pass
|
@@ -52,6 +59,7 @@ class WarpJob:
|
|
52
59
|
noisolation=False,
|
53
60
|
context=None,
|
54
61
|
run_offset=0,
|
62
|
+
n_subjobs=1, # Total Number of Runs
|
55
63
|
):
|
56
64
|
self.project_id = project_id
|
57
65
|
self.job_id = os.environ.get("ARTEFACTS_JOB_ID", None)
|
@@ -67,28 +75,27 @@ class WarpJob:
|
|
67
75
|
self.noupload = noupload
|
68
76
|
self.noisolation = noisolation
|
69
77
|
self.context = context
|
78
|
+
self.n_subjobs = n_subjobs
|
70
79
|
|
71
80
|
if dryrun:
|
72
81
|
self.job_id = "dryrun"
|
73
82
|
if self.job_id is None:
|
74
83
|
# Only create a new job if job_id is not specified
|
75
84
|
data = {
|
85
|
+
"project_id": self.project_id,
|
76
86
|
"start": round(self.start),
|
77
87
|
"status": "in progress",
|
78
88
|
"params": json.dumps(self.params),
|
79
89
|
"project": self.project_id,
|
80
90
|
"jobname": self.jobname,
|
81
91
|
"timeout": self.params.get("timeout", 5) * 60,
|
92
|
+
"n_subjobs": self.n_subjobs,
|
82
93
|
}
|
83
94
|
if context is not None:
|
84
95
|
data["message"] = context["description"]
|
85
96
|
data["commit"] = context["commit"]
|
86
97
|
data["ref"] = context["ref"]
|
87
|
-
response =
|
88
|
-
f"{api_conf.api_url}/{self.project_id}/job",
|
89
|
-
json=data,
|
90
|
-
headers=api_conf.headers,
|
91
|
-
)
|
98
|
+
response = self.api_conf.create("job", data)
|
92
99
|
if response.status_code != 200:
|
93
100
|
if response.status_code == 403:
|
94
101
|
msg = response.json()["message"]
|
@@ -102,7 +109,7 @@ class WarpJob:
|
|
102
109
|
)
|
103
110
|
)
|
104
111
|
logger.warning(response.text)
|
105
|
-
raise
|
112
|
+
raise ArtefactsAPIError(str(response.status_code))
|
106
113
|
self.job_id = response.json()["job_id"]
|
107
114
|
self.output_path = self.params.get("output_path", f"/tmp/{self.job_id}")
|
108
115
|
os.makedirs(self.output_path, exist_ok=True)
|
@@ -111,24 +118,21 @@ class WarpJob:
|
|
111
118
|
def log_tests_result(self, success):
|
112
119
|
self.success = success
|
113
120
|
|
114
|
-
def
|
121
|
+
def update(self, last_run_success: bool) -> bool:
|
115
122
|
end = datetime.now(timezone.utc).timestamp()
|
116
123
|
if self.dryrun:
|
117
|
-
return
|
124
|
+
return True
|
118
125
|
# Log metadata
|
119
126
|
data = {
|
127
|
+
"project_id": self.project_id,
|
120
128
|
"end": round(end),
|
121
129
|
"duration": round(end - self.start),
|
122
|
-
"success":
|
130
|
+
"success": last_run_success,
|
123
131
|
"status": "finished", # need to be determined based on all runs
|
124
132
|
}
|
125
|
-
|
126
|
-
f"{self.api_conf.api_url}/{self.project_id}/job/{self.job_id}",
|
127
|
-
json=data,
|
128
|
-
headers=self.api_conf.headers,
|
129
|
-
)
|
133
|
+
response = self.api_conf.update("job", self.job_id, data)
|
130
134
|
|
131
|
-
return
|
135
|
+
return response.status_code == 200
|
132
136
|
|
133
137
|
def new_run(self, scenario):
|
134
138
|
run = WarpRun(self, scenario["name"], scenario, self.n_runs)
|
@@ -153,6 +157,7 @@ class WarpRun:
|
|
153
157
|
self.logger = logger
|
154
158
|
os.makedirs(self.output_path, exist_ok=True)
|
155
159
|
data = {
|
160
|
+
"project_id": self.job.project_id,
|
156
161
|
"job_id": job.job_id,
|
157
162
|
"run_n": self.run_n,
|
158
163
|
"start": round(self.start),
|
@@ -163,14 +168,8 @@ class WarpRun:
|
|
163
168
|
|
164
169
|
if self.job.dryrun:
|
165
170
|
return
|
166
|
-
|
167
|
-
|
168
|
-
)
|
169
|
-
response = requests.post(
|
170
|
-
query_url,
|
171
|
-
json=data,
|
172
|
-
headers=self.job.api_conf.headers,
|
173
|
-
)
|
171
|
+
|
172
|
+
response = self.job.api_conf.create("run", data)
|
174
173
|
if response.status_code != 200:
|
175
174
|
if response.status_code == 403:
|
176
175
|
msg = response.json()["message"]
|
@@ -184,7 +183,7 @@ class WarpRun:
|
|
184
183
|
)
|
185
184
|
)
|
186
185
|
self.logger.warning(response.text)
|
187
|
-
raise
|
186
|
+
raise ArtefactsAPIError(str(response.status_code))
|
188
187
|
return
|
189
188
|
|
190
189
|
def log_params(self, params):
|
@@ -266,6 +265,7 @@ class WarpRun:
|
|
266
265
|
|
267
266
|
# Log metadata
|
268
267
|
data = {
|
268
|
+
"project_id": self.job.project_id,
|
269
269
|
"job_id": self.job.job_id,
|
270
270
|
"run_n": self.run_n,
|
271
271
|
"start": math.floor(self.start),
|
@@ -280,11 +280,7 @@ class WarpRun:
|
|
280
280
|
if not self.job.noupload:
|
281
281
|
data["uploads"] = self.uploads
|
282
282
|
|
283
|
-
response =
|
284
|
-
f"{self.job.api_conf.api_url}/{self.job.project_id}/job/{self.job.job_id}/run/{self.run_n}",
|
285
|
-
json=data,
|
286
|
-
headers=self.job.api_conf.headers,
|
287
|
-
)
|
283
|
+
response = self.job.api_conf.update("run", self.run_n, data)
|
288
284
|
|
289
285
|
# use s3 presigned urls to upload the artifacts
|
290
286
|
if self.job.noupload:
|
@@ -307,11 +303,8 @@ class WarpRun:
|
|
307
303
|
)
|
308
304
|
)
|
309
305
|
)
|
310
|
-
|
311
|
-
|
312
|
-
upload_info["url"],
|
313
|
-
data=upload_info["fields"],
|
314
|
-
files=files,
|
306
|
+
self.job.api_conf.upload(
|
307
|
+
upload_info["url"], upload_info["fields"], files
|
315
308
|
)
|
316
309
|
except OverflowError:
|
317
310
|
self.logger.warning(
|
@@ -342,6 +335,7 @@ def init_job(
|
|
342
335
|
noisolation: bool = False,
|
343
336
|
context: Optional[dict] = None,
|
344
337
|
run_offset=0,
|
338
|
+
n_subjobs: int = 1,
|
345
339
|
):
|
346
340
|
return WarpJob(
|
347
341
|
project_id,
|
@@ -354,6 +348,7 @@ def init_job(
|
|
354
348
|
noisolation,
|
355
349
|
context,
|
356
350
|
run_offset,
|
351
|
+
n_subjobs,
|
357
352
|
)
|
358
353
|
|
359
354
|
|
artefacts/cli/app.py
CHANGED
@@ -20,6 +20,7 @@ from artefacts.cli import (
|
|
20
20
|
init_job,
|
21
21
|
generate_scenarios,
|
22
22
|
localise,
|
23
|
+
logger,
|
23
24
|
AuthenticationError,
|
24
25
|
__version__,
|
25
26
|
)
|
@@ -32,6 +33,7 @@ from artefacts.cli.constants import (
|
|
32
33
|
)
|
33
34
|
from artefacts.cli.helpers import (
|
34
35
|
add_key_to_conf,
|
36
|
+
endpoint_exists,
|
35
37
|
get_conf_from_file,
|
36
38
|
get_artefacts_api_url,
|
37
39
|
get_git_revision_branch,
|
@@ -64,39 +66,70 @@ def add(project_name):
|
|
64
66
|
profile = config[project_name]
|
65
67
|
else:
|
66
68
|
profile = {}
|
69
|
+
|
67
70
|
api_url = get_artefacts_api_url(profile)
|
68
71
|
dashboard_url = api_url.split("/api")[0]
|
72
|
+
|
69
73
|
settings_page_url = f"{dashboard_url}/{project_name}/settings"
|
70
|
-
# Check if running on WSL
|
71
|
-
if "WSLENV" in os.environ:
|
72
|
-
os.system(f'cmd.exe /C start "" {settings_page_url} 2>/dev/null')
|
73
|
-
else:
|
74
|
-
webbrowser.open(settings_page_url)
|
75
74
|
click.echo(
|
76
75
|
localise("Opening the project settings page: {url}").format(
|
77
76
|
url=settings_page_url
|
78
77
|
)
|
79
78
|
)
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
79
|
+
|
80
|
+
if endpoint_exists(settings_page_url):
|
81
|
+
# Check if running on WSL
|
82
|
+
if "WSLENV" in os.environ:
|
83
|
+
os.system(f'cmd.exe /C start "" {settings_page_url} 2>/dev/null')
|
84
|
+
else:
|
85
|
+
webbrowser.open(settings_page_url)
|
86
|
+
|
87
|
+
api_key = click.prompt(
|
88
|
+
localise("Please enter your API KEY for {project}").format(
|
89
|
+
project=project_name
|
90
|
+
),
|
91
|
+
type=str,
|
92
|
+
hide_input=True,
|
93
|
+
)
|
94
|
+
add_key_to_conf(project_name, api_key)
|
95
|
+
click.echo(localise("API KEY saved for {project}").format(project=project_name))
|
96
|
+
if click.confirm(
|
97
|
+
localise(
|
98
|
+
"Would you like to download a pregenerated artefacts.yaml file? This will overwrite any existing config file in the current directory."
|
99
|
+
)
|
100
|
+
):
|
101
|
+
config_file_name = "artefacts.yaml"
|
102
|
+
config_file_url = f"{api_url}/{project_name}/{config_file_name}"
|
103
|
+
# Get credentials only now, as API key set just before.
|
104
|
+
api_conf = APIConf(project_name, __version__)
|
105
|
+
config_response = api_conf.read("url", config_file_url)
|
106
|
+
if config_response.status_code == 200:
|
107
|
+
with open(config_file_name, "wb") as f:
|
108
|
+
f.write(config_response.content)
|
109
|
+
else:
|
110
|
+
click.echo(
|
111
|
+
localise(
|
112
|
+
"We encountered a problem in getting the generated configuration file. Please consider downloading it from the project page on the dashboard at {url}. Sorry for the inconvenience."
|
113
|
+
).format(url=settings_page_url)
|
114
|
+
)
|
115
|
+
logger.debug(
|
116
|
+
localise(
|
117
|
+
"If you are using an alternative server, please also consider checking the value of ARTEFACTS_API_URL in your environment."
|
118
|
+
)
|
119
|
+
)
|
120
|
+
else:
|
121
|
+
click.echo(
|
122
|
+
localise(
|
123
|
+
"Our apologies: The project page does not seem available at the moment. If `{project_name}` is correct, please try again later.".format(
|
124
|
+
project_name=project_name
|
125
|
+
)
|
126
|
+
)
|
127
|
+
)
|
128
|
+
click.echo(
|
129
|
+
localise(
|
130
|
+
"If you are using an alternative server, please also consider checking the value of ARTEFACTS_API_URL in your environment."
|
131
|
+
)
|
92
132
|
)
|
93
|
-
):
|
94
|
-
api_conf = APIConf(project_name, __version__)
|
95
|
-
config_file_name = "artefacts.yaml"
|
96
|
-
config_file_url = f"{api_url}/{project_name}/{config_file_name}"
|
97
|
-
r = requests.get(config_file_url, headers=api_conf.headers)
|
98
|
-
with open(config_file_name, "wb") as f:
|
99
|
-
f.write(r.content)
|
100
133
|
return
|
101
134
|
|
102
135
|
|
@@ -118,10 +151,7 @@ def delete(project_name):
|
|
118
151
|
def hello(project_name):
|
119
152
|
"""Show message to confirm credentials allow access to PROJECT_NAME"""
|
120
153
|
api_conf = APIConf(project_name, __version__)
|
121
|
-
response =
|
122
|
-
f"{api_conf.api_url}/{project_name}/info",
|
123
|
-
headers=api_conf.headers,
|
124
|
-
)
|
154
|
+
response = api_conf.read("url", f"{api_conf.api_url}/{project_name}/info")
|
125
155
|
if response.status_code == 200:
|
126
156
|
result = response.json()
|
127
157
|
click.echo(
|
@@ -360,6 +390,7 @@ def run(
|
|
360
390
|
noisolation,
|
361
391
|
context,
|
362
392
|
first,
|
393
|
+
len(scenarios),
|
363
394
|
)
|
364
395
|
except AuthenticationError:
|
365
396
|
click.secho(
|
@@ -412,6 +443,8 @@ def run(
|
|
412
443
|
)
|
413
444
|
run.log_tests_results([result], False)
|
414
445
|
run.stop()
|
446
|
+
warpjob.update(last_run_success=False)
|
447
|
+
continue
|
415
448
|
if dryrun:
|
416
449
|
click.echo(f"[{jobname}] " + localise("Performing dry run"))
|
417
450
|
results, success = {}, True
|
@@ -419,7 +452,8 @@ def run(
|
|
419
452
|
try:
|
420
453
|
results, success = run_ros2_tests(run)
|
421
454
|
except Exception as e:
|
422
|
-
|
455
|
+
run.stop()
|
456
|
+
warpjob.update(last_run_success=False)
|
423
457
|
warpjob.log_tests_result(False)
|
424
458
|
click.secho(e, bold=True, err=True)
|
425
459
|
click.secho(
|
@@ -428,10 +462,18 @@ def run(
|
|
428
462
|
err=True,
|
429
463
|
bold=True,
|
430
464
|
)
|
431
|
-
|
465
|
+
continue
|
432
466
|
if success is None:
|
467
|
+
result = get_TestSuite_error_result(
|
468
|
+
scenario["name"],
|
469
|
+
localise("ROS2 environment error"),
|
470
|
+
localise(
|
471
|
+
"Not able to execute tests. Make sure that ROS2 is sourced and that your launch file syntax is correct."
|
472
|
+
),
|
473
|
+
)
|
474
|
+
run.log_tests_results([result], False)
|
433
475
|
run.stop()
|
434
|
-
warpjob.
|
476
|
+
warpjob.update(last_run_success=False)
|
435
477
|
warpjob.log_tests_result(job_success)
|
436
478
|
click.secho(
|
437
479
|
f"[{jobname}] "
|
@@ -441,7 +483,7 @@ def run(
|
|
441
483
|
err=True,
|
442
484
|
bold=True,
|
443
485
|
)
|
444
|
-
|
486
|
+
continue
|
445
487
|
if not success:
|
446
488
|
job_success = False
|
447
489
|
elif framework is not None and framework.startswith("ros1:"):
|
@@ -454,7 +496,17 @@ def run(
|
|
454
496
|
err=True,
|
455
497
|
bold=True,
|
456
498
|
)
|
457
|
-
|
499
|
+
result = get_TestSuite_error_result(
|
500
|
+
scenario["name"],
|
501
|
+
localise("launch_test file not specified error"),
|
502
|
+
localise(
|
503
|
+
"Please specify a `ros_testfile` in the artefacts.yaml scenario configuration."
|
504
|
+
),
|
505
|
+
)
|
506
|
+
run.log_tests_results([result], False)
|
507
|
+
run.stop()
|
508
|
+
warpjob.update(last_run_success=False)
|
509
|
+
continue
|
458
510
|
if dryrun:
|
459
511
|
click.echo(f"[{jobname}] " + localise("Performing dry run"))
|
460
512
|
results, success = {}, True
|
@@ -472,7 +524,10 @@ def run(
|
|
472
524
|
err=True,
|
473
525
|
bold=True,
|
474
526
|
)
|
475
|
-
|
527
|
+
run.stop()
|
528
|
+
warpjob.update(last_run_success=False)
|
529
|
+
warpjob.log_tests_result(False)
|
530
|
+
continue
|
476
531
|
if dryrun:
|
477
532
|
click.echo(f"[{jobname}] " + localise("Performing dry run"))
|
478
533
|
results, success = {}, True
|
@@ -487,12 +542,11 @@ def run(
|
|
487
542
|
add_output_from_default(run)
|
488
543
|
|
489
544
|
run.stop()
|
490
|
-
|
545
|
+
warpjob.log_tests_result(job_success)
|
546
|
+
warpjob.update(last_run_success=run.success)
|
491
547
|
click.echo(f"[{jobname}] " + localise("Done"))
|
492
548
|
time.sleep(random.random() * 1)
|
493
549
|
|
494
|
-
warpjob.stop()
|
495
|
-
|
496
550
|
|
497
551
|
@click.command()
|
498
552
|
@click.option(
|
@@ -590,9 +644,10 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
590
644
|
temp_file.seek(0)
|
591
645
|
|
592
646
|
# Request signed upload URLs
|
593
|
-
|
594
|
-
|
595
|
-
|
647
|
+
# TODO Horrible approach, but the endpoint is neither CRUD nor REST, etc.
|
648
|
+
# So the idea to remain horrible and change this soon.
|
649
|
+
upload_urls_response = api_conf.direct("put")(
|
650
|
+
f"{api_conf.api_url}/{project_id}/upload_source"
|
596
651
|
)
|
597
652
|
|
598
653
|
if not upload_urls_response.ok:
|
@@ -694,10 +749,10 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
694
749
|
item_show_func=lambda x: x and x[0],
|
695
750
|
) as bar:
|
696
751
|
for filename, file in bar:
|
697
|
-
response =
|
752
|
+
response = api_conf.upload(
|
698
753
|
upload_urls[filename]["url"],
|
699
|
-
|
700
|
-
|
754
|
+
upload_urls[filename]["fields"],
|
755
|
+
{"file": file},
|
701
756
|
)
|
702
757
|
if not response.ok:
|
703
758
|
raise click.ClickException(
|
artefacts/cli/config.py
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from contextlib import contextmanager
|
3
|
+
from functools import partial
|
4
|
+
import logging
|
1
5
|
import os
|
2
6
|
import platform
|
3
|
-
from typing import Optional
|
7
|
+
from typing import Optional, Tuple
|
4
8
|
|
5
9
|
import click
|
10
|
+
from requests import Response, Session
|
11
|
+
from requests.adapters import HTTPAdapter
|
12
|
+
from requests.exceptions import ConnectionError
|
13
|
+
from urllib3.util import Retry
|
6
14
|
|
7
15
|
from artefacts.cli.i18n import localise
|
8
16
|
from artefacts.cli.helpers import (
|
@@ -10,10 +18,18 @@ from artefacts.cli.helpers import (
|
|
10
18
|
get_artefacts_api_url,
|
11
19
|
)
|
12
20
|
|
21
|
+
# Mask warnings from urllib, typically when it retries failed API calls
|
22
|
+
urllib3logger = logging.getLogger("urllib3")
|
23
|
+
urllib3logger.setLevel(logging.ERROR)
|
24
|
+
|
13
25
|
|
14
26
|
class APIConf:
|
15
27
|
def __init__(
|
16
|
-
self,
|
28
|
+
self,
|
29
|
+
project_name: str,
|
30
|
+
api_version: str,
|
31
|
+
job_name: Optional[str] = None,
|
32
|
+
session: Optional[Session] = None,
|
17
33
|
) -> None:
|
18
34
|
config = get_conf_from_file()
|
19
35
|
if project_name in config:
|
@@ -60,3 +76,135 @@ class APIConf:
|
|
60
76
|
)
|
61
77
|
)
|
62
78
|
)
|
79
|
+
|
80
|
+
#
|
81
|
+
# Retry settings
|
82
|
+
#
|
83
|
+
self.session = session or Session()
|
84
|
+
retries = Retry(
|
85
|
+
total=3,
|
86
|
+
backoff_factor=0.1,
|
87
|
+
status_forcelist=[502, 503, 504],
|
88
|
+
allowed_methods=Retry.DEFAULT_ALLOWED_METHODS | {"POST"},
|
89
|
+
)
|
90
|
+
# Default connect timeout set to a small value above the default 3s for TCP
|
91
|
+
# Default read timeout a typical value. Does not scale when too aggressive
|
92
|
+
# (note: read timeout is between byte sent, not the whole read)
|
93
|
+
self.request_timeout = (3.03, 27)
|
94
|
+
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
95
|
+
|
96
|
+
@contextmanager
|
97
|
+
def _api(self):
|
98
|
+
try:
|
99
|
+
yield self.session
|
100
|
+
except ConnectionError as e:
|
101
|
+
raise click.ClickException(
|
102
|
+
localise(
|
103
|
+
"Unable to complete the operation: Network error.\n"
|
104
|
+
"This may be a problem with an Artefacts server, or your network.\n"
|
105
|
+
"Please try again in a moment or confirm your internet connection.\n"
|
106
|
+
"If the problem persists, please contact us (info@artefacts.com)!\n"
|
107
|
+
f"All we know: {e}"
|
108
|
+
)
|
109
|
+
)
|
110
|
+
|
111
|
+
def _conn_info(self, obj: str, data: Optional[dict] = None) -> Tuple[str, dict]:
|
112
|
+
"""
|
113
|
+
Prepare connection information for a given resource kind (`obj`).
|
114
|
+
|
115
|
+
Returns a tuple (url, payload), where url is the endpoint for
|
116
|
+
the resource, and payload the prepared data that needs be sent.
|
117
|
+
|
118
|
+
Note the prepared data does not validate the content. It simply
|
119
|
+
remove any extra data used internally to the code here.
|
120
|
+
"""
|
121
|
+
try:
|
122
|
+
if "url" == obj:
|
123
|
+
return obj, None
|
124
|
+
elif "job" == obj:
|
125
|
+
return f"{self.api_url}/{data['project_id']}/job", data
|
126
|
+
elif "run" == obj:
|
127
|
+
project_id = data.pop("project_id")
|
128
|
+
return f"{self.api_url}/{project_id}/job/{data['job_id']}/run", data
|
129
|
+
else:
|
130
|
+
raise Exception(
|
131
|
+
f"Unable to determine API URL for unknown object kind: {obj}"
|
132
|
+
)
|
133
|
+
except KeyError as e:
|
134
|
+
raise Exception(f"Missing parameter for building a {obj} URL: {e}")
|
135
|
+
|
136
|
+
def create(self, obj: str, data: dict) -> Response:
|
137
|
+
"""
|
138
|
+
Create a resource. Typical for endpoints of the form POST /obj
|
139
|
+
"""
|
140
|
+
url, payload = self._conn_info(obj, data)
|
141
|
+
with self._api() as session:
|
142
|
+
return session.post(
|
143
|
+
url,
|
144
|
+
json=payload,
|
145
|
+
headers=self.headers,
|
146
|
+
timeout=self.request_timeout,
|
147
|
+
)
|
148
|
+
|
149
|
+
def read(self, obj: str, obj_id: Optional[str]) -> Response:
|
150
|
+
"""
|
151
|
+
Read a resource content. Typical for endpoints of the form GET /obj/id
|
152
|
+
"""
|
153
|
+
url, _ = self._conn_info(obj)
|
154
|
+
if obj_id:
|
155
|
+
url = f"{url}/{obj_id}"
|
156
|
+
with self._api() as session:
|
157
|
+
return session.get(
|
158
|
+
url,
|
159
|
+
headers=self.headers,
|
160
|
+
timeout=self.request_timeout,
|
161
|
+
)
|
162
|
+
|
163
|
+
def update(self, obj: str, obj_id: str, data: dict) -> Response:
|
164
|
+
"""
|
165
|
+
Update (modify) a resource content. Typical for endpoints of the form PUT /obj/id
|
166
|
+
"""
|
167
|
+
url, payload = self._conn_info(obj, data)
|
168
|
+
with self._api() as session:
|
169
|
+
return session.put(
|
170
|
+
f"{url}/{obj_id}",
|
171
|
+
json=payload,
|
172
|
+
headers=self.headers,
|
173
|
+
timeout=self.request_timeout,
|
174
|
+
)
|
175
|
+
|
176
|
+
def upload(self, url: str, data: dict, files: list) -> Response:
|
177
|
+
"""
|
178
|
+
Upload files.
|
179
|
+
|
180
|
+
Note this is temporary helper, as we expect to turn files as
|
181
|
+
first-order resource at the API level, so move to CRUD model, etc.
|
182
|
+
|
183
|
+
This facility disables all timeouts, as uploads can be very
|
184
|
+
long, and we'd better wait.
|
185
|
+
"""
|
186
|
+
with self._api() as session:
|
187
|
+
return session.post(
|
188
|
+
url,
|
189
|
+
data=data,
|
190
|
+
files=files,
|
191
|
+
timeout=None,
|
192
|
+
)
|
193
|
+
|
194
|
+
def direct(self, verb: str) -> Callable:
|
195
|
+
"""
|
196
|
+
Direct access to the common session.
|
197
|
+
|
198
|
+
Important: This exposes this object session. It is not the
|
199
|
+
guarded session from self._api, because:
|
200
|
+
1. This is temporary anyway to accommodate irregular API
|
201
|
+
calls (that is, breach to CRUD/REST models).
|
202
|
+
2. Using a context manager leads to "leaking" the session,
|
203
|
+
without the context extras (as this returns and so exits
|
204
|
+
the context).
|
205
|
+
"""
|
206
|
+
return partial(
|
207
|
+
getattr(self.session, verb),
|
208
|
+
headers=self.headers,
|
209
|
+
timeout=self.request_timeout,
|
210
|
+
)
|
artefacts/cli/helpers.py
CHANGED
@@ -2,6 +2,8 @@ import configparser
|
|
2
2
|
import os
|
3
3
|
import subprocess
|
4
4
|
|
5
|
+
import requests
|
6
|
+
|
5
7
|
from artefacts.cli.constants import CONFIG_PATH, CONFIG_DIR
|
6
8
|
|
7
9
|
|
@@ -53,3 +55,13 @@ def add_key_to_conf(project_name, api_key):
|
|
53
55
|
config[project_name] = {"ApiKey": api_key}
|
54
56
|
with open(CONFIG_PATH, "w") as f:
|
55
57
|
config.write(f)
|
58
|
+
|
59
|
+
|
60
|
+
def endpoint_exists(url: str) -> bool:
|
61
|
+
"""
|
62
|
+
Simplistic confirmation of the existance of an endpoint.
|
63
|
+
|
64
|
+
Under discussion: Use of HEAD verbs, etc.
|
65
|
+
"""
|
66
|
+
access_test = requests.get(url)
|
67
|
+
return access_test.status_code < 400
|