artefacts-cli 0.6.12__tar.gz → 0.6.16__tar.gz

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 (63) hide show
  1. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/PKG-INFO +2 -1
  2. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/app.py +13 -50
  3. artefacts_cli-0.6.16/artefacts/cli/app_containers.py +77 -0
  4. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/constants.py +2 -0
  5. artefacts_cli-0.6.16/artefacts/cli/containers/__init__.py +61 -0
  6. artefacts_cli-0.6.16/artefacts/cli/containers/docker.py +86 -0
  7. artefacts_cli-0.6.16/artefacts/cli/containers/utils.py +54 -0
  8. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/ros2.py +1 -1
  9. artefacts_cli-0.6.16/artefacts/cli/utils.py +99 -0
  10. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/version.py +2 -2
  11. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts_cli.egg-info/PKG-INFO +2 -1
  12. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts_cli.egg-info/SOURCES.txt +7 -1
  13. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts_cli.egg-info/requires.txt +1 -0
  14. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/pyproject.toml +1 -0
  15. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/pytest.ini +1 -1
  16. artefacts_cli-0.6.16/tests/cli/test_app_containers.py +58 -0
  17. artefacts_cli-0.6.16/tests/conftest.py +42 -0
  18. artefacts_cli-0.6.16/tests/utils/docker_mock.py +89 -0
  19. artefacts_cli-0.6.12/artefacts/cli/utils.py +0 -35
  20. artefacts_cli-0.6.12/tests/conftest.py +0 -8
  21. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/README.md +0 -0
  22. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/README_INTERNAL.md +0 -0
  23. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/__init__.py +0 -0
  24. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/bagparser.py +0 -0
  25. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/other.py +0 -0
  26. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/parameters.py +0 -0
  27. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/ros1.py +0 -0
  28. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/cli/utils_ros.py +0 -0
  29. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts/wrappers/artefacts_ros1_meta.launch +0 -0
  30. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts.yaml +0 -0
  31. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts_cli.egg-info/dependency_links.txt +0 -0
  32. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts_cli.egg-info/entry_points.txt +0 -0
  33. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/artefacts_cli.egg-info/top_level.txt +0 -0
  34. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/bin/release +0 -0
  35. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/CMakeLists.txt +0 -0
  36. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_meta.launch +0 -0
  37. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_turtle.launch +0 -0
  38. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/turtle_odometry.launch +0 -0
  39. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/package.xml +0 -0
  40. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/setup.py +0 -0
  41. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/TestTurtle.py +0 -0
  42. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/__init__.py +0 -0
  43. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_odom.py +0 -0
  44. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_post_process.py +0 -0
  45. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_trajectory.py +0 -0
  46. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/test/viz_turtle_odom.xml +0 -0
  47. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim2/launch_turtle.py +0 -0
  48. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/infra-tests/turtlesim2/sample_node.py +0 -0
  49. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/setup.cfg +0 -0
  50. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/__init__.py +0 -0
  51. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/__init__.py +0 -0
  52. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/test_cli.py +0 -0
  53. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/test_config_validation.py +0 -0
  54. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/test_other.py +0 -0
  55. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/test_parameters.py +0 -0
  56. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/test_ros1.py +0 -0
  57. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/test_ros2.py +0 -0
  58. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/cli/test_warp.py +0 -0
  59. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/fixtures/artefacts_deprecated.yaml +0 -0
  60. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/fixtures/artefacts_ros1.yaml +0 -0
  61. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/fixtures/warp-env-param.yaml +0 -0
  62. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/fixtures/warp.yaml +0 -0
  63. {artefacts_cli-0.6.12 → artefacts_cli-0.6.16}/tests/test_config_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: artefacts_cli
3
- Version: 0.6.12
3
+ Version: 0.6.16
4
4
  Author-email: FD <fabian@artefacts.com>, AGC <alejandro@artefacts.com>, TN <tomo@artefacts.com>, EP <eric@artefacts.com>
5
5
  Project-URL: Homepage, https://github.com/art-e-fact/artefacts-client
6
6
  Project-URL: Bug Tracker, https://github.com/art-e-fact/artefacts-client/issues
@@ -21,6 +21,7 @@ Requires-Dist: setuptools-scm
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: awscli; extra == "dev"
23
23
  Requires-Dist: build; extra == "dev"
24
+ Requires-Dist: docker; extra == "dev"
24
25
  Requires-Dist: lark; extra == "dev"
25
26
  Requires-Dist: pyre-check; extra == "dev"
26
27
  Requires-Dist: pytest; extra == "dev"
@@ -9,7 +9,6 @@ import sys
9
9
  import tarfile
10
10
  import tempfile
11
11
  import time
12
- from typing import Any, Union
13
12
  from urllib.parse import urlparse
14
13
  import webbrowser
15
14
 
@@ -20,7 +19,9 @@ from pathlib import Path
20
19
  from gitignore_parser import parse_gitignore
21
20
 
22
21
  from artefacts.cli import init_job, generate_scenarios, AuthenticationError, __version__
22
+ from artefacts.cli import app_containers as containers
23
23
  from artefacts.cli.constants import DEPRECATED_FRAMEWORKS, SUPPORTED_FRAMEWORKS
24
+ from artefacts.cli.utils import read_config, config_validation
24
25
  import artefacts_copava as copava
25
26
 
26
27
  HOME = os.path.expanduser("~")
@@ -100,55 +101,10 @@ class APIConf:
100
101
  click.echo(f"Connecting to {self.api_url} using {auth_type}")
101
102
 
102
103
 
103
- def read_config(filename: str) -> dict:
104
- try:
105
- with open(filename) as f:
106
- return copava.parse(f.read())
107
- except FileNotFoundError:
108
- raise click.ClickException(f"Project config file {filename} not found.")
109
-
110
-
111
104
  def validate_artefacts_config(config_file: str) -> dict:
112
105
  pass
113
106
 
114
107
 
115
- def pretty_print_config_error(
116
- errors: Union[str, list, dict], indent: int = 0, prefix: str = "", suffix: str = ""
117
- ) -> str:
118
- if type(errors) is str:
119
- header = " " * indent
120
- output = header + prefix + errors + suffix
121
- elif type(errors) is list:
122
- _depth = indent + 1
123
- output = []
124
- for value in errors:
125
- output.append(pretty_print_config_error(value, indent=_depth, prefix="- "))
126
- output = os.linesep.join(output)
127
- elif type(errors) is dict:
128
- _depth = indent + 1
129
- output = []
130
- for key, value in errors.items():
131
- output.append(pretty_print_config_error(key, indent=indent, suffix=":"))
132
- output.append(pretty_print_config_error(value, indent=_depth))
133
- output = os.linesep.join(output)
134
- else:
135
- # Must not happen, so broad definition, but we want to know fast.
136
- raise Exception(f"Unacceptable data type for config error formatting: {errors}")
137
- return output
138
-
139
-
140
- # Click callback syntax
141
- def config_validation(context: dict, param: str, value: str) -> str:
142
- if context.params["skip_validation"]:
143
- return value
144
- config = read_config(value)
145
- errors = copava.check(config)
146
- if len(errors) == 0:
147
- return value
148
- else:
149
- raise click.BadParameter(pretty_print_config_error(errors))
150
-
151
-
152
108
  @click.group()
153
109
  def config():
154
110
  return
@@ -334,7 +290,7 @@ def run(
334
290
  )
335
291
  except AuthenticationError:
336
292
  raise click.ClickException(
337
- "Unable to authenticate, check your Project Name and API Key"
293
+ "Unable to authenticate (Stage: Job initialisation), please check your project name and API key"
338
294
  )
339
295
 
340
296
  job_success = True
@@ -346,7 +302,7 @@ def run(
346
302
  run = warpjob.new_run(scenario)
347
303
  except AuthenticationError:
348
304
  raise click.ClickException(
349
- "Unable to authenticate, check your Project Name and API Key"
305
+ "Unable to authenticate (Stage: Job run), please check your project name and API key"
350
306
  )
351
307
  if framework is not None and framework.startswith("ros2:"):
352
308
  from artefacts.cli.ros2 import run_ros2_tests
@@ -474,7 +430,7 @@ def run_remote(config, description, jobname, skip_validation=False):
474
430
  click.echo(f"Packaging source...")
475
431
 
476
432
  with tempfile.NamedTemporaryFile(
477
- prefix=project_id, suffix=".tgz", delete=True
433
+ prefix=project_id.split("/")[-1], suffix=".tgz", delete=True
478
434
  ) as temp_file:
479
435
  # get list of patterns to be ignored in .artefactsignore
480
436
  ignore_file = Path(project_folder) / Path(".artefactsignore")
@@ -512,7 +468,13 @@ def run_remote(config, description, jobname, skip_validation=False):
512
468
  )
513
469
 
514
470
  if not upload_urls_response.ok:
515
- result = upload_urls_response.json()
471
+ try:
472
+ result = upload_urls_response.json()
473
+ except requests.exceptions.JSONDecodeError:
474
+ raise click.ClickException(
475
+ f"Apologies, problem in interacting with the Artefacts backend: {upload_urls_response.status_code} {upload_urls_response.reason}. Response text: {upload_urls_response.text}."
476
+ )
477
+
516
478
  if (
517
479
  upload_urls_response.status_code == 403
518
480
  and result["message"] == "Not allowed"
@@ -611,6 +573,7 @@ artefacts.add_command(config)
611
573
  artefacts.add_command(hello)
612
574
  artefacts.add_command(run)
613
575
  artefacts.add_command(run_remote)
576
+ artefacts.add_command(containers.package)
614
577
 
615
578
 
616
579
  if __name__ == "__main__":
@@ -0,0 +1,77 @@
1
+ import os
2
+
3
+ import click
4
+
5
+ from artefacts.cli.constants import DEFAULT_API_URL
6
+ from artefacts.cli.utils import config_validation, read_config
7
+ from artefacts.cli.containers.utils import ContainerMgr
8
+
9
+
10
+ @click.group()
11
+ @click.option("--debug/--no-debug", default=False)
12
+ @click.pass_context
13
+ def package(ctx: click.Context, debug: bool):
14
+ ctx.ensure_object(dict)
15
+ ctx.obj["debug"] = debug
16
+
17
+
18
+ @package.command()
19
+ @click.option(
20
+ "--path",
21
+ default=".",
22
+ help="Path to the root of the project, where a Dockerfile is available.",
23
+ )
24
+ @click.option(
25
+ "--dockerfile",
26
+ default="Dockerfile",
27
+ help="File name of the container definition file. Defaults to the standard Dockerfile inside the project root (see --path)",
28
+ )
29
+ @click.option(
30
+ "--name",
31
+ required=False,
32
+ help="Name for the generated package",
33
+ )
34
+ @click.pass_context
35
+ def build(ctx: click.Context, path: str, dockerfile: str, name: str):
36
+ if not os.path.exists(os.path.join(path, dockerfile)):
37
+ raise click.ClickException(
38
+ f"No {dockerfile} found here. I cannot build the package."
39
+ )
40
+ if name is None:
41
+ name = "artefacts"
42
+ handler = ContainerMgr()
43
+ image, _ = handler.build(path=path, name=name)
44
+ print(f"Package complete in image: {image}")
45
+
46
+
47
+ @package.command()
48
+ @click.argument("image")
49
+ @click.argument("jobname")
50
+ @click.option(
51
+ "--config",
52
+ callback=config_validation,
53
+ default="artefacts.yaml",
54
+ help="Artefacts config file.",
55
+ )
56
+ @click.pass_context
57
+ def run(ctx: click.Context, image: str, jobname: str, config: str):
58
+ try:
59
+ artefacts_config = read_config(config)
60
+ except FileNotFoundError:
61
+ raise click.ClickException(f"Project config file not found: {config}")
62
+ project = artefacts_config["project"]
63
+ handler = ContainerMgr()
64
+ params = dict(
65
+ image=image,
66
+ project=project,
67
+ jobname=jobname,
68
+ # Hidden setting primarily useful to Artefacts developers
69
+ api_url=os.environ.get("ARTEFACTS_API_URL", DEFAULT_API_URL),
70
+ )
71
+ container, logs = handler.run(**params)
72
+ if container:
73
+ print(f"Package run complete: Container Id for inspection: {container['Id']}")
74
+ else:
75
+ print(f"Package run failed:")
76
+ for entry in logs:
77
+ print("\t- " + entry)
@@ -1,3 +1,5 @@
1
+ DEFAULT_API_URL = "https://app.artefacts.com/api"
2
+
1
3
  SUPPORTED_FRAMEWORKS = [
2
4
  "ros2:iron",
3
5
  "ros2:humble",
@@ -0,0 +1,61 @@
1
+ from collections.abc import Generator
2
+ import configparser
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Tuple, Union
6
+
7
+ from artefacts.cli.constants import DEFAULT_API_URL
8
+
9
+
10
+ class CMgr:
11
+ def build(self, **kwargs) -> Tuple[str, Generator]:
12
+ """
13
+ Returns the build image ID (e.g. sha256:abcdefghi)
14
+ and an iterator over the build log entries.
15
+ """
16
+ raise NotImplemented()
17
+
18
+ def run(
19
+ self,
20
+ image: str,
21
+ project: str,
22
+ jobname: str = None,
23
+ artefacts_dir: str = Path("~/.artefacts").expanduser(),
24
+ api_url: str = DEFAULT_API_URL,
25
+ ) -> Tuple[Any, Generator]:
26
+ """
27
+ Returns a container (Any type as depends on the framework)
28
+ and an iterator over the container log entries.
29
+ """
30
+ raise NotImplemented()
31
+
32
+ def _valid_artefacts_api_key(
33
+ self, project: str, path: Union[str, Path] = Path("~/.artefacts").expanduser()
34
+ ) -> bool:
35
+ """
36
+ Check if a valid API key is available to embed in containers.
37
+
38
+ 1. Check overrides with the ARTEFACTS_KEY environment variable.
39
+ 2. If `path` is not given, check the default .artefacts folder for the config file.
40
+ 3. If `path` is given, check the file directly is a file, or check for a `config` file if a folder.
41
+
42
+ When a config file is found, we check here if the API key for the `project` is available.
43
+
44
+ `path` set to None is an error, and aborts execution.
45
+ """
46
+ if not path:
47
+ raise Exception(
48
+ "`path` must be a string, a Path object, or exclusded from the kwargs"
49
+ )
50
+ if os.environ.get("ARTEFACTS_KEY", None):
51
+ return True
52
+ path = Path(path) # Ensure we have a Path object
53
+ config = configparser.ConfigParser()
54
+ if path.is_dir():
55
+ config.read(path / "config")
56
+ else:
57
+ config.read(path)
58
+ try:
59
+ return config[project].get("apikey") != None
60
+ except KeyError:
61
+ return False
@@ -0,0 +1,86 @@
1
+ from collections.abc import Generator
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Tuple
6
+ from uuid import uuid4
7
+
8
+ from artefacts.cli.constants import DEFAULT_API_URL
9
+ from artefacts.cli.containers import CMgr
10
+ from artefacts.cli.utils import ensure_available
11
+
12
+ ensure_available("docker")
13
+
14
+ import docker
15
+ from docker import APIClient
16
+
17
+
18
+ class DockerManager(CMgr):
19
+ def __init__(self):
20
+ self.client = APIClient()
21
+
22
+ def build(self, **kwargs) -> Tuple[str, Generator]:
23
+ kwargs["tag"] = kwargs.pop("name")
24
+ logs = []
25
+ img_id = None
26
+ for entry in self.client.build(**kwargs):
27
+ line_data = [
28
+ json.loads(v) for v in entry.decode("utf-8").split("\r\n") if len(v) > 0
29
+ ]
30
+ for data in line_data:
31
+ if "stream" in data:
32
+ line = data["stream"].strip()
33
+ if not line.startswith("---") and len(line) > 0:
34
+ print(line)
35
+ logs.append(line)
36
+ elif "aux" in data and "ID" in data["aux"]:
37
+ img_id = data["aux"]["ID"]
38
+ if img_id is None:
39
+ img_id = self.client.inspect_image(kwargs["tag"])["Id"]
40
+ return img_id, iter(logs)
41
+
42
+ def run(
43
+ self,
44
+ image: str,
45
+ project: str,
46
+ jobname: str = None,
47
+ artefacts_dir: str = Path("~/.artefacts").expanduser(),
48
+ api_url: str = DEFAULT_API_URL,
49
+ ) -> Tuple[Any, Generator]:
50
+ if not self._valid_artefacts_api_key(project, artefacts_dir):
51
+ return None, iter(
52
+ [
53
+ "Missing API key for the project. Does `~/.artefacts/config` exist and contain your key?"
54
+ ]
55
+ )
56
+ try:
57
+ env = {
58
+ "JOB_ID": str(uuid4()),
59
+ "ARTEFACTS_JOB_NAME": jobname,
60
+ "ARTEFACTS_API_URL": api_url,
61
+ }
62
+
63
+ container = self.client.create_container(
64
+ image,
65
+ environment=env,
66
+ detach=False,
67
+ volumes=["/root/.artefacts"],
68
+ host_config=self.client.create_host_config(
69
+ binds={
70
+ artefacts_dir: {
71
+ "bind": "/root/.artefacts",
72
+ "mode": "ro",
73
+ },
74
+ },
75
+ ),
76
+ )
77
+ self.client.start(container=container.get("Id"))
78
+ for entry in self.client.logs(container=container.get("Id"), stream=True):
79
+ print(entry.decode("utf-8").strip())
80
+ return container, iter([])
81
+ except docker.errors.ImageNotFound:
82
+ return None, iter(
83
+ [f"Image {image} not found by Docker. Perhaps need to build first?"]
84
+ )
85
+ except Exception as e:
86
+ return None, iter([f"Failed to run from {image}. All we know: {e}"])
@@ -0,0 +1,54 @@
1
+ from collections.abc import Generator
2
+ import logging
3
+ from typing import Any, Tuple
4
+
5
+ from artefacts.cli.containers import CMgr
6
+
7
+
8
+ class ContainerMgr:
9
+ SUPPORTED_PRIORITISED_ENGINES = {
10
+ 1: "docker",
11
+ # 2: "podman",
12
+ }
13
+
14
+ def __init__(self):
15
+ self.logger = logging.getLogger(__name__)
16
+ self.mgr = self._configure()
17
+ if self.mgr is None:
18
+ raise Exception(
19
+ f"Failed to find supported container stack. Please install and start one in {list(self.SUPPORTED_PRIORITISED_ENGINES.values())}, with default settings (custom sockets not supported at this stage)"
20
+ )
21
+
22
+ def _configure(self) -> CMgr:
23
+ manager = None
24
+ for idx in sorted(self.SUPPORTED_PRIORITISED_ENGINES):
25
+ engine = self.SUPPORTED_PRIORITISED_ENGINES[idx]
26
+ try:
27
+ handler = getattr(self, f"_configure_{engine}")
28
+ manager = handler()
29
+ except AttributeError:
30
+ self.logger.warning(
31
+ f"Tried to detect an unsupported engine: {engine}. WIP? Ignore and continue."
32
+ )
33
+ except Exception as e:
34
+ self.logger.warning(
35
+ f"Problem in detecting {engine} ({e}) Ignore and continue."
36
+ )
37
+ if manager is not None:
38
+ break
39
+ return manager
40
+
41
+ def _configure_docker(self):
42
+ from artefacts.cli.containers.docker import DockerManager
43
+
44
+ return DockerManager()
45
+
46
+ # def _configure_podman(self):
47
+ # from artefacts.cli.containers import podman
48
+ # return PodmanManager()
49
+
50
+ def build(self, **kwargs) -> Tuple[str, Generator]:
51
+ return self.mgr.build(**kwargs)
52
+
53
+ def run(self, **kwargs) -> Tuple[Any, Generator]:
54
+ return self.mgr.run(**kwargs)
@@ -94,7 +94,7 @@ def run_ros2_tests(run):
94
94
  # check if any rosbag was created
95
95
  rosbags = glob("rosbag2*")
96
96
  new_rosbags = set(rosbags).difference(set(preexisting_rosbags))
97
- from artefacts.bagparser import BagFileParser
97
+ from artefacts.cli.bagparser import BagFileParser
98
98
 
99
99
  if len(new_rosbags) > 0:
100
100
  rosbag_path = new_rosbags.pop()
@@ -0,0 +1,99 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from typing import Any, Union
5
+
6
+ import click
7
+
8
+ import artefacts_copava as copava
9
+
10
+
11
+ def run_and_save_logs(
12
+ args, output_path, shell=False, executable=None, env=None, cwd=None
13
+ ):
14
+ """
15
+ Run a command and save stdout and stderr to a file in output_path
16
+
17
+ Note: explicitly list used named params instead of using **kwargs to avoid typing issue: https://github.com/microsoft/pyright/issues/455#issuecomment-780076232
18
+ """
19
+ output_file = open(output_path, "wb")
20
+ proc = subprocess.Popen(
21
+ args,
22
+ stdout=subprocess.PIPE, # Capture stdout
23
+ stderr=subprocess.PIPE, # Capture stderr
24
+ shell=shell,
25
+ executable=executable,
26
+ env=env,
27
+ cwd=cwd,
28
+ )
29
+ # write test-process stdout and stderr into file and stdout
30
+ if proc.stdout:
31
+ for line in proc.stdout:
32
+ decoded_line = line.decode()
33
+ sys.stdout.write(decoded_line)
34
+ output_file.write(line)
35
+ if proc.stderr:
36
+ for line in proc.stderr:
37
+ decoded_line = line.decode()
38
+ sys.stderr.write(decoded_line)
39
+ output_file.write(line)
40
+ proc.wait()
41
+ return proc.returncode
42
+
43
+
44
+ def ensure_available(package: str) -> None:
45
+ import importlib
46
+
47
+ try:
48
+ importlib.import_module(package)
49
+ except ImportError:
50
+ """
51
+ Recommended by the Python community
52
+ https://pip.pypa.io/en/latest/user_guide/#using-pip-from-your-program
53
+ """
54
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
55
+
56
+
57
+ def read_config(filename: str) -> dict:
58
+ try:
59
+ with open(filename) as f:
60
+ return copava.parse(f.read())
61
+ except FileNotFoundError:
62
+ raise click.ClickException(f"Project config file {filename} not found.")
63
+
64
+
65
+ # Click callback syntax
66
+ def config_validation(context: dict, param: str, value: str) -> str:
67
+ if context.params.get("skip_validation", False):
68
+ return value
69
+ config = read_config(value)
70
+ errors = copava.check(config)
71
+ if len(errors) == 0:
72
+ return value
73
+ else:
74
+ raise click.BadParameter(pretty_print_config_error(errors))
75
+
76
+
77
+ def pretty_print_config_error(
78
+ errors: Union[str, list, dict], indent: int = 0, prefix: str = "", suffix: str = ""
79
+ ) -> str:
80
+ if type(errors) is str:
81
+ header = " " * indent
82
+ output = header + prefix + errors + suffix
83
+ elif type(errors) is list:
84
+ _depth = indent + 1
85
+ output = []
86
+ for value in errors:
87
+ output.append(pretty_print_config_error(value, indent=_depth, prefix="- "))
88
+ output = os.linesep.join(output)
89
+ elif type(errors) is dict:
90
+ _depth = indent + 1
91
+ output = []
92
+ for key, value in errors.items():
93
+ output.append(pretty_print_config_error(key, indent=indent, suffix=":"))
94
+ output.append(pretty_print_config_error(value, indent=_depth))
95
+ output = os.linesep.join(output)
96
+ else:
97
+ # Must not happen, so broad definition, but we want to know fast.
98
+ raise Exception(f"Unacceptable data type for config error formatting: {errors}")
99
+ return output
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.6.12'
16
- __version_tuple__ = version_tuple = (0, 6, 12)
15
+ __version__ = version = '0.6.16'
16
+ __version_tuple__ = version_tuple = (0, 6, 16)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: artefacts_cli
3
- Version: 0.6.12
3
+ Version: 0.6.16
4
4
  Author-email: FD <fabian@artefacts.com>, AGC <alejandro@artefacts.com>, TN <tomo@artefacts.com>, EP <eric@artefacts.com>
5
5
  Project-URL: Homepage, https://github.com/art-e-fact/artefacts-client
6
6
  Project-URL: Bug Tracker, https://github.com/art-e-fact/artefacts-client/issues
@@ -21,6 +21,7 @@ Requires-Dist: setuptools-scm
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: awscli; extra == "dev"
23
23
  Requires-Dist: build; extra == "dev"
24
+ Requires-Dist: docker; extra == "dev"
24
25
  Requires-Dist: lark; extra == "dev"
25
26
  Requires-Dist: pyre-check; extra == "dev"
26
27
  Requires-Dist: pytest; extra == "dev"
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  pytest.ini
6
6
  artefacts/cli/__init__.py
7
7
  artefacts/cli/app.py
8
+ artefacts/cli/app_containers.py
8
9
  artefacts/cli/bagparser.py
9
10
  artefacts/cli/constants.py
10
11
  artefacts/cli/other.py
@@ -14,6 +15,9 @@ artefacts/cli/ros2.py
14
15
  artefacts/cli/utils.py
15
16
  artefacts/cli/utils_ros.py
16
17
  artefacts/cli/version.py
18
+ artefacts/cli/containers/__init__.py
19
+ artefacts/cli/containers/docker.py
20
+ artefacts/cli/containers/utils.py
17
21
  artefacts/wrappers/artefacts_ros1_meta.launch
18
22
  artefacts_cli.egg-info/PKG-INFO
19
23
  artefacts_cli.egg-info/SOURCES.txt
@@ -40,6 +44,7 @@ tests/__init__.py
40
44
  tests/conftest.py
41
45
  tests/test_config_validation.py
42
46
  tests/cli/__init__.py
47
+ tests/cli/test_app_containers.py
43
48
  tests/cli/test_cli.py
44
49
  tests/cli/test_config_validation.py
45
50
  tests/cli/test_other.py
@@ -50,4 +55,5 @@ tests/cli/test_warp.py
50
55
  tests/fixtures/artefacts_deprecated.yaml
51
56
  tests/fixtures/artefacts_ros1.yaml
52
57
  tests/fixtures/warp-env-param.yaml
53
- tests/fixtures/warp.yaml
58
+ tests/fixtures/warp.yaml
59
+ tests/utils/docker_mock.py
@@ -11,6 +11,7 @@ setuptools-scm
11
11
  [dev]
12
12
  awscli
13
13
  build
14
+ docker
14
15
  lark
15
16
  pyre-check
16
17
  pytest
@@ -42,6 +42,7 @@ local_scheme = "no-local-version"
42
42
  dev = [
43
43
  "awscli",
44
44
  "build",
45
+ "docker",
45
46
  "lark",
46
47
  "pyre-check",
47
48
  "pytest",
@@ -1,5 +1,5 @@
1
1
  [pytest]
2
- addopts = -ra --quiet --cov-config .coveragerc --cov-report term-missing --cov-fail-under 25
2
+ addopts = -ra --cov-config .coveragerc --cov-report term-missing --cov-fail-under 25
3
3
  console_output_style = progress
4
4
  env =
5
5
  ARTEFACTS_CLI_ENV=test
@@ -0,0 +1,58 @@
1
+ import os
2
+ from uuid import uuid4
3
+
4
+ import pytest
5
+
6
+ from artefacts.cli import app_containers as containers
7
+
8
+
9
+ def test_container_package_exists(cli_runner):
10
+ result = cli_runner.invoke(containers.package, [])
11
+ assert result.exit_code == 0
12
+
13
+
14
+ def test_container_package_build_specific_dockerfile(
15
+ cli_runner, dockerfile_available, docker_mocker
16
+ ):
17
+ dockerfile = "non_standard_dockerfile"
18
+ result = cli_runner.invoke(
19
+ containers.package, ["build", "--dockerfile", dockerfile]
20
+ )
21
+ dockerfile_available.assert_any_call(os.path.join(".", dockerfile))
22
+ assert result.exit_code == 0
23
+
24
+
25
+ def test_container_package_build_specific_dockerfile_missing(
26
+ cli_runner, dockerfile_not_available
27
+ ):
28
+ dockerfile = "non_standard_dockerfile"
29
+ result = cli_runner.invoke(
30
+ containers.package, ["build", "--dockerfile", dockerfile]
31
+ )
32
+ dockerfile_not_available.assert_any_call(os.path.join(".", dockerfile))
33
+ assert result.exit_code == 1
34
+ assert (
35
+ result.output.strip()
36
+ == f"Error: No {dockerfile} found here. I cannot build the package."
37
+ )
38
+
39
+
40
+ def test_container_package_build_specific_image_name(
41
+ cli_runner, dockerfile_available, docker_mocker
42
+ ):
43
+ name = str(uuid4())
44
+ before = len(docker_mocker.images())
45
+ result = cli_runner.invoke(containers.package, ["build", "--name", name])
46
+ assert result.exit_code == 0
47
+ assert len(docker_mocker.images()) == before + 1
48
+ assert docker_mocker.get_image(name).Repository == name
49
+
50
+
51
+ def test_container_package_build_default_image_name(
52
+ cli_runner, dockerfile_available, docker_mocker
53
+ ):
54
+ before = len(docker_mocker.images())
55
+ result = cli_runner.invoke(containers.package, ["build"])
56
+ assert result.exit_code == 0
57
+ assert len(docker_mocker.images()) == before + 1
58
+ assert docker_mocker.get_image("artefacts") is not None
@@ -0,0 +1,42 @@
1
+ import pytest
2
+
3
+ import os
4
+
5
+ from click.testing import CliRunner
6
+ import docker
7
+
8
+ from tests.utils import docker_mock
9
+
10
+
11
+ def dockerfile_presence(mocker, value: bool):
12
+ original = os.path.exists
13
+
14
+ def exists(path):
15
+ if "dockerfile" in path.lower():
16
+ return value
17
+ else:
18
+ return original(path)
19
+
20
+ return mocker.patch("os.path.exists", side_effect=exists, autospec=True)
21
+
22
+
23
+ @pytest.fixture(scope="function")
24
+ def dockerfile_available(mocker):
25
+ return dockerfile_presence(mocker, True)
26
+
27
+
28
+ @pytest.fixture(scope="function")
29
+ def dockerfile_not_available(mocker):
30
+ return dockerfile_presence(mocker, False)
31
+
32
+
33
+ @pytest.fixture(scope="module")
34
+ def docker_mocker(module_mocker):
35
+ test_client = docker_mock.make_fake_api_client()
36
+ module_mocker.patch("docker.APIClient", return_value=test_client)
37
+ return test_client
38
+
39
+
40
+ @pytest.fixture(scope="module")
41
+ def cli_runner():
42
+ return CliRunner()
@@ -0,0 +1,89 @@
1
+ # Fake Docker client
2
+
3
+ import copy
4
+ from dataclasses import dataclass, asdict
5
+ import hashlib
6
+ from random import randint
7
+ from unittest import mock
8
+
9
+ from docker import DockerClient, APIClient
10
+ from docker.constants import DEFAULT_DOCKER_API_VERSION
11
+
12
+
13
+ @dataclass
14
+ class SimpleImage:
15
+ Id: str
16
+ Created: str
17
+ Repository: str
18
+ RepoTags: list
19
+
20
+
21
+ class TestDockerAPIClient(APIClient):
22
+ def __init__(self, *args, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+ self.fake_server_db = {}
25
+ self.fake_image_id_base = hashlib.new("sha256")
26
+
27
+ def build(self, *args, **kwargs):
28
+ if kwargs["tag"] in self.fake_server_db:
29
+ original = self.fake_server_db[kwargs["tag"]]
30
+ for img in original:
31
+ # Mimick what Docker seems to be doing
32
+ img.Repository = ""
33
+ img.RepoTags = []
34
+ else:
35
+ original = []
36
+
37
+ # Generate a random yet compliant image ID
38
+ # Note the use of update, to ensure calling many times will be on different input (so unique IDs).
39
+ self.fake_image_id_base.update(bytes(randint(0, 100)))
40
+ iid = "sha256:" + self.fake_image_id_base.hexdigest()
41
+
42
+ new_collection = [
43
+ SimpleImage(
44
+ **{
45
+ "Id": iid,
46
+ "Created": "1 min ago",
47
+ "Repository": kwargs["tag"],
48
+ "RepoTags": [kwargs["tag"] + ":latest"],
49
+ }
50
+ )
51
+ ]
52
+ # Ensure the new entry is in the first place
53
+ new_collection.extend(original)
54
+ self.fake_server_db[kwargs["tag"]] = new_collection
55
+
56
+ return [b'"fake"', b'"test"', b'"logs"']
57
+
58
+ def images(self, *args, **kwargs):
59
+ if kwargs.get("name", None):
60
+ return self.fake_server_db.get(kwargs["name"])
61
+ else:
62
+ return [v for vs in self.fake_server_db.values() for v in vs]
63
+
64
+ def get_image(self, name):
65
+ # Index 0 is the latest
66
+ return self.fake_server_db.get(name)[0]
67
+
68
+ def inspect_image(self, name):
69
+ return asdict(self.get_image(name))
70
+
71
+
72
+ class TestDockerClient(DockerClient):
73
+ def __init__(self, *args, **kwargs):
74
+ super().__init__(*args, **kwargs)
75
+ self.api = TestDockerAPIClient(version=kwargs["version"])
76
+
77
+
78
+ def make_fake_api_client():
79
+ """
80
+ Incomplete fake API client.
81
+ """
82
+ return TestDockerAPIClient(version=DEFAULT_DOCKER_API_VERSION)
83
+
84
+
85
+ def make_fake_client():
86
+ """
87
+ Test client with a fake API client.
88
+ """
89
+ return TestDockerClient(version=DEFAULT_DOCKER_API_VERSION)
@@ -1,35 +0,0 @@
1
- import subprocess
2
- import sys
3
-
4
-
5
- def run_and_save_logs(
6
- args, output_path, shell=False, executable=None, env=None, cwd=None
7
- ):
8
- """
9
- Run a command and save stdout and stderr to a file in output_path
10
-
11
- Note: explicitly list used named params instead of using **kwargs to avoid typing issue: https://github.com/microsoft/pyright/issues/455#issuecomment-780076232
12
- """
13
- output_file = open(output_path, "wb")
14
- proc = subprocess.Popen(
15
- args,
16
- stdout=subprocess.PIPE, # Capture stdout
17
- stderr=subprocess.PIPE, # Capture stderr
18
- shell=shell,
19
- executable=executable,
20
- env=env,
21
- cwd=cwd,
22
- )
23
- # write test-process stdout and stderr into file and stdout
24
- if proc.stdout:
25
- for line in proc.stdout:
26
- decoded_line = line.decode()
27
- sys.stdout.write(decoded_line)
28
- output_file.write(line)
29
- if proc.stderr:
30
- for line in proc.stderr:
31
- decoded_line = line.decode()
32
- sys.stderr.write(decoded_line)
33
- output_file.write(line)
34
- proc.wait()
35
- return proc.returncode
@@ -1,8 +0,0 @@
1
- import pytest
2
-
3
- from click.testing import CliRunner
4
-
5
-
6
- @pytest.fixture(scope="module")
7
- def cli_runner():
8
- return CliRunner()
File without changes
File without changes