artefacts-cli 0.9.2__py3-none-any.whl → 0.9.4__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 +16 -31
- artefacts/cli/app.py +98 -47
- artefacts/cli/config.py +140 -1
- artefacts/cli/helpers.py +12 -0
- artefacts/cli/locales/art.pot +96 -64
- artefacts/cli/locales/base.pot +96 -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.2.dist-info → artefacts_cli-0.9.4.dist-info}/METADATA +2 -1
- {artefacts_cli-0.9.2.dist-info → artefacts_cli-0.9.4.dist-info}/RECORD +16 -16
- {artefacts_cli-0.9.2.dist-info → artefacts_cli-0.9.4.dist-info}/WHEEL +0 -0
- {artefacts_cli-0.9.2.dist-info → artefacts_cli-0.9.4.dist-info}/entry_points.txt +0 -0
- {artefacts_cli-0.9.2.dist-info → artefacts_cli-0.9.4.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
|
@@ -52,6 +51,7 @@ class WarpJob:
|
|
52
51
|
noisolation=False,
|
53
52
|
context=None,
|
54
53
|
run_offset=0,
|
54
|
+
n_subjobs=1, # Total Number of Runs
|
55
55
|
):
|
56
56
|
self.project_id = project_id
|
57
57
|
self.job_id = os.environ.get("ARTEFACTS_JOB_ID", None)
|
@@ -67,6 +67,7 @@ class WarpJob:
|
|
67
67
|
self.noupload = noupload
|
68
68
|
self.noisolation = noisolation
|
69
69
|
self.context = context
|
70
|
+
self.n_subjobs = n_subjobs
|
70
71
|
|
71
72
|
if dryrun:
|
72
73
|
self.job_id = "dryrun"
|
@@ -79,16 +80,13 @@ class WarpJob:
|
|
79
80
|
"project": self.project_id,
|
80
81
|
"jobname": self.jobname,
|
81
82
|
"timeout": self.params.get("timeout", 5) * 60,
|
83
|
+
"n_subjobs": self.n_subjobs,
|
82
84
|
}
|
83
85
|
if context is not None:
|
84
86
|
data["message"] = context["description"]
|
85
87
|
data["commit"] = context["commit"]
|
86
88
|
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
|
-
)
|
89
|
+
response = self.api_conf.create("job", data)
|
92
90
|
if response.status_code != 200:
|
93
91
|
if response.status_code == 403:
|
94
92
|
msg = response.json()["message"]
|
@@ -111,7 +109,7 @@ class WarpJob:
|
|
111
109
|
def log_tests_result(self, success):
|
112
110
|
self.success = success
|
113
111
|
|
114
|
-
def
|
112
|
+
def update(self, last_run_success: bool):
|
115
113
|
end = datetime.now(timezone.utc).timestamp()
|
116
114
|
if self.dryrun:
|
117
115
|
return
|
@@ -119,14 +117,10 @@ class WarpJob:
|
|
119
117
|
data = {
|
120
118
|
"end": round(end),
|
121
119
|
"duration": round(end - self.start),
|
122
|
-
"success":
|
120
|
+
"success": last_run_success,
|
123
121
|
"status": "finished", # need to be determined based on all runs
|
124
122
|
}
|
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
|
-
)
|
123
|
+
self.api_conf.update("job", self.job_id, data)
|
130
124
|
|
131
125
|
return
|
132
126
|
|
@@ -153,6 +147,7 @@ class WarpRun:
|
|
153
147
|
self.logger = logger
|
154
148
|
os.makedirs(self.output_path, exist_ok=True)
|
155
149
|
data = {
|
150
|
+
"project_id": self.job.project_id,
|
156
151
|
"job_id": job.job_id,
|
157
152
|
"run_n": self.run_n,
|
158
153
|
"start": round(self.start),
|
@@ -163,14 +158,8 @@ class WarpRun:
|
|
163
158
|
|
164
159
|
if self.job.dryrun:
|
165
160
|
return
|
166
|
-
|
167
|
-
|
168
|
-
)
|
169
|
-
response = requests.post(
|
170
|
-
query_url,
|
171
|
-
json=data,
|
172
|
-
headers=self.job.api_conf.headers,
|
173
|
-
)
|
161
|
+
|
162
|
+
response = self.job.api_conf.create("run", data)
|
174
163
|
if response.status_code != 200:
|
175
164
|
if response.status_code == 403:
|
176
165
|
msg = response.json()["message"]
|
@@ -266,6 +255,7 @@ class WarpRun:
|
|
266
255
|
|
267
256
|
# Log metadata
|
268
257
|
data = {
|
258
|
+
"project_id": self.job.project_id,
|
269
259
|
"job_id": self.job.job_id,
|
270
260
|
"run_n": self.run_n,
|
271
261
|
"start": math.floor(self.start),
|
@@ -280,11 +270,7 @@ class WarpRun:
|
|
280
270
|
if not self.job.noupload:
|
281
271
|
data["uploads"] = self.uploads
|
282
272
|
|
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
|
-
)
|
273
|
+
response = self.job.api_conf.update("run", self.run_n, data)
|
288
274
|
|
289
275
|
# use s3 presigned urls to upload the artifacts
|
290
276
|
if self.job.noupload:
|
@@ -307,11 +293,8 @@ class WarpRun:
|
|
307
293
|
)
|
308
294
|
)
|
309
295
|
)
|
310
|
-
|
311
|
-
|
312
|
-
upload_info["url"],
|
313
|
-
data=upload_info["fields"],
|
314
|
-
files=files,
|
296
|
+
self.job.api_conf.upload(
|
297
|
+
upload_info["url"], upload_info["fields"], files
|
315
298
|
)
|
316
299
|
except OverflowError:
|
317
300
|
self.logger.warning(
|
@@ -342,6 +325,7 @@ def init_job(
|
|
342
325
|
noisolation: bool = False,
|
343
326
|
context: Optional[dict] = None,
|
344
327
|
run_offset=0,
|
328
|
+
n_subjobs: int = 1,
|
345
329
|
):
|
346
330
|
return WarpJob(
|
347
331
|
project_id,
|
@@ -354,6 +338,7 @@ def init_job(
|
|
354
338
|
noisolation,
|
355
339
|
context,
|
356
340
|
run_offset,
|
341
|
+
n_subjobs,
|
357
342
|
)
|
358
343
|
|
359
344
|
|
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(
|
@@ -551,10 +605,6 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
551
605
|
# Use the same logic as `run` for expanding scenarios based on array params
|
552
606
|
job["scenarios"]["settings"], _ = generate_scenarios(job, None)
|
553
607
|
|
554
|
-
# Ensure unique names
|
555
|
-
for idx, scenario in enumerate(job["scenarios"]["settings"]):
|
556
|
-
scenario["name"] = f"{scenario['name']}-{idx}"
|
557
|
-
|
558
608
|
run_config["jobs"] = {jobname: job}
|
559
609
|
if "on" in run_config:
|
560
610
|
del run_config["on"]
|
@@ -594,9 +644,10 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
594
644
|
temp_file.seek(0)
|
595
645
|
|
596
646
|
# Request signed upload URLs
|
597
|
-
|
598
|
-
|
599
|
-
|
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"
|
600
651
|
)
|
601
652
|
|
602
653
|
if not upload_urls_response.ok:
|
@@ -698,10 +749,10 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
698
749
|
item_show_func=lambda x: x and x[0],
|
699
750
|
) as bar:
|
700
751
|
for filename, file in bar:
|
701
|
-
response =
|
752
|
+
response = api_conf.upload(
|
702
753
|
upload_urls[filename]["url"],
|
703
|
-
|
704
|
-
|
754
|
+
upload_urls[filename]["fields"],
|
755
|
+
{"file": file},
|
705
756
|
)
|
706
757
|
if not response.ok:
|
707
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,6 +18,10 @@ 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__(
|
@@ -60,3 +72,130 @@ class APIConf:
|
|
60
72
|
)
|
61
73
|
)
|
62
74
|
)
|
75
|
+
|
76
|
+
#
|
77
|
+
# Retry settings
|
78
|
+
#
|
79
|
+
self.session = Session()
|
80
|
+
retries = Retry(
|
81
|
+
total=3,
|
82
|
+
backoff_factor=0.1,
|
83
|
+
status_forcelist=[502, 503, 504],
|
84
|
+
allowed_methods=Retry.DEFAULT_ALLOWED_METHODS | {"POST"},
|
85
|
+
)
|
86
|
+
# Default connect timeout set to a small value above the default 3s for TCP
|
87
|
+
# Default read timeout a bit aggressive to ensure snappy experience
|
88
|
+
# (note: read timeout is between byte sent, not the whole read)
|
89
|
+
self.request_timeout = (3.03, 7)
|
90
|
+
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
91
|
+
|
92
|
+
@contextmanager
|
93
|
+
def _api(self):
|
94
|
+
try:
|
95
|
+
yield self.session
|
96
|
+
except ConnectionError:
|
97
|
+
raise click.ClickException(
|
98
|
+
localise(
|
99
|
+
"Unable to interact with the Artefacts API at this time. "
|
100
|
+
"Our apologies, this looks like us. Please retry "
|
101
|
+
"later, or perhaps confirm your internet connectivity."
|
102
|
+
)
|
103
|
+
)
|
104
|
+
|
105
|
+
def _conn_info(self, obj: str, data: Optional[dict] = None) -> Tuple[str, dict]:
|
106
|
+
"""
|
107
|
+
Prepare connection information for a given resource kind (`obj`).
|
108
|
+
|
109
|
+
Returns a tuple (url, payload), where url is the endpoint for
|
110
|
+
the resource, and payload the prepared data that needs be sent.
|
111
|
+
|
112
|
+
Note the prepared data does not validate the content. It simply
|
113
|
+
remove any extra data used internally to the code here.
|
114
|
+
"""
|
115
|
+
try:
|
116
|
+
if "url" == obj:
|
117
|
+
return obj, None
|
118
|
+
elif "job" == obj:
|
119
|
+
return f"{self.api_url}/{data['project_id']}/job", data
|
120
|
+
elif "run" == obj:
|
121
|
+
project_id = data.pop("project_id")
|
122
|
+
return f"{self.api_url}/{project_id}/job/{data['job_id']}/run", data
|
123
|
+
else:
|
124
|
+
raise Exception(
|
125
|
+
f"Unable to determine API URL for unknown object kind: {obj}"
|
126
|
+
)
|
127
|
+
except KeyError as e:
|
128
|
+
raise Exception(f"Missing parameter for building a {obj} URL: {e}")
|
129
|
+
|
130
|
+
def create(self, obj: str, data: dict) -> Response:
|
131
|
+
"""
|
132
|
+
Create a resource. Typical for endpoints of the form POST /obj
|
133
|
+
"""
|
134
|
+
url, payload = self._conn_info(obj, data)
|
135
|
+
with self._api() as session:
|
136
|
+
return session.post(
|
137
|
+
url,
|
138
|
+
json=payload,
|
139
|
+
headers=self.headers,
|
140
|
+
timeout=self.request_timeout,
|
141
|
+
)
|
142
|
+
|
143
|
+
def read(self, obj: str, obj_id: Optional[str]) -> Response:
|
144
|
+
"""
|
145
|
+
Read a resource content. Typical for endpoints of the form GET /obj/id
|
146
|
+
"""
|
147
|
+
url, _ = self._conn_info(obj)
|
148
|
+
if obj_id:
|
149
|
+
url = f"{url}/{obj_id}"
|
150
|
+
with self._api() as session:
|
151
|
+
return session.get(
|
152
|
+
url,
|
153
|
+
headers=self.headers,
|
154
|
+
timeout=self.request_timeout,
|
155
|
+
)
|
156
|
+
|
157
|
+
def update(self, obj: str, obj_id: str, data: dict) -> Response:
|
158
|
+
"""
|
159
|
+
Update (modify) a resource content. Typical for endpoints of the form PUT /obj/id
|
160
|
+
"""
|
161
|
+
url, payload = self._conn_info(obj, data)
|
162
|
+
with self._api() as session:
|
163
|
+
return session.put(
|
164
|
+
f"{url}/{obj_id}",
|
165
|
+
json=payload,
|
166
|
+
headers=self.headers,
|
167
|
+
timeout=self.request_timeout,
|
168
|
+
)
|
169
|
+
|
170
|
+
def upload(self, url: str, data: dict, files: list) -> Response:
|
171
|
+
"""
|
172
|
+
Upload files.
|
173
|
+
|
174
|
+
Note this is temporary helper, as we expect to turn files as
|
175
|
+
first-order resource at the API level, so move to CRUD model, etc.
|
176
|
+
"""
|
177
|
+
with self._api() as session:
|
178
|
+
return session.post(
|
179
|
+
url,
|
180
|
+
data=data,
|
181
|
+
files=files,
|
182
|
+
timeout=self.request_timeout,
|
183
|
+
)
|
184
|
+
|
185
|
+
def direct(self, verb: str) -> Callable:
|
186
|
+
"""
|
187
|
+
Direct access to the common session.
|
188
|
+
|
189
|
+
Important: This exposes this object session. It is not the
|
190
|
+
guarded session from self._api, because:
|
191
|
+
1. This is temporary anyway to accommodate irregular API
|
192
|
+
calls (that is, breach to CRUD/REST models).
|
193
|
+
2. Using a context manager leads to "leaking" the session,
|
194
|
+
without the context extras (as this returns and so exits
|
195
|
+
the context).
|
196
|
+
"""
|
197
|
+
return partial(
|
198
|
+
getattr(self.session, verb),
|
199
|
+
headers=self.headers,
|
200
|
+
timeout=self.request_timeout,
|
201
|
+
)
|
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
|