partcad-cli 0.3.1__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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: partcad-cli
3
+ Version: 0.3.1
4
+ Summary: Command-line interface to PartCAD
5
+ Author-email: Roman Kuzmenko <openvmp@proton.me>
6
+ Maintainer-email: Roman Kuzmenko <openvmp@proton.me>
7
+ Project-URL: Homepage, https://github.com/openvmp/partcad
8
+ Project-URL: Issues, https://github.com/openvmp/partcad/issues
9
+ Keywords: cadquery,build123d,cad,design,openscad,step,stl
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: partcad==0.3.1
16
+ Requires-Dist: pycairo
17
+ Requires-Dist: renderlab
18
+ Requires-Dist: rlPyCairo
19
+ Requires-Dist: svglib
20
+ Requires-Dist: ocp_vscode
21
+ Requires-Dist: progress
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "partcad-cli"
3
+ version = "0.3.1"
4
+ description = "Command-line interface to PartCAD"
5
+ readme = "README.md"
6
+ keywords = ["cadquery", "build123d", "cad", "design", "openscad", "step", "stl"]
7
+ requires-python = ">=3.7"
8
+ license = {file = "LICENSE.txt"}
9
+ authors = [
10
+ {name = "Roman Kuzmenko", email = "openvmp@proton.me" }
11
+ ]
12
+ maintainers = [
13
+ {name = "Roman Kuzmenko", email = "openvmp@proton.me" }
14
+ ]
15
+ dynamic = ["dependencies"]
16
+
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Operating System :: OS Independent",
21
+ ]
22
+
23
+ [tool.setuptools.package-data]
24
+ "partcad_cli.template" = ["*.yaml"]
25
+
26
+ [project.scripts]
27
+ pc = "partcad_cli:main_cli"
28
+
29
+ [tool.setuptools.dynamic]
30
+ dependencies = {file = ["requirements.txt"]}
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/openvmp/partcad"
34
+ Issues = "https://github.com/openvmp/partcad/issues"
@@ -0,0 +1,7 @@
1
+ partcad==0.3.1
2
+ pycairo
3
+ renderlab
4
+ rlPyCairo
5
+ svglib
6
+ ocp_vscode
7
+ progress
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ from .cli import main as main_cli
2
+
3
+ __all__ = [
4
+ "main_cli",
5
+ ]
6
+
7
+ __version__: str = "0.3.1"
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # OpenVMP, 2023
4
+ #
5
+ # Author: Roman Kuzmenko
6
+ # Created: 2023-12-22
7
+ #
8
+ # Licensed under Apache License, Version 2.0.
9
+ #
10
+
11
+ import argparse
12
+ import logging
13
+
14
+ import partcad as pc
15
+
16
+ from .cli_add import *
17
+ from .cli_init import *
18
+ from .cli_install import *
19
+ from .cli_list import *
20
+ from .cli_render import *
21
+ from .cli_show import *
22
+
23
+
24
+ def main():
25
+ parser = argparse.ArgumentParser(
26
+ description="PartCAD command line tool",
27
+ )
28
+ parser.add_argument(
29
+ "-p",
30
+ help="Package path (a YAML file or a directory with 'partcad.yaml')",
31
+ type=str,
32
+ default=None,
33
+ dest="config_path",
34
+ )
35
+ # TODO(clairbee): add a config option to change logging mechanism and level
36
+ subparsers = parser.add_subparsers(dest="command")
37
+ cli_help_add(subparsers)
38
+ cli_help_init(subparsers)
39
+ cli_help_install(subparsers)
40
+ cli_help_list(subparsers)
41
+ cli_help_render(subparsers)
42
+ cli_help_show(subparsers)
43
+
44
+ args = parser.parse_args()
45
+
46
+ logging.basicConfig(level=logging.INFO)
47
+
48
+ if args.command == "add":
49
+ cli_add(args)
50
+
51
+ elif args.command == "init":
52
+ cli_init(args)
53
+
54
+ elif args.command == "install":
55
+ cli_install(args)
56
+
57
+ elif args.command == "list":
58
+ cli_list(args)
59
+
60
+ elif args.command == "list-all":
61
+ cli_list_assemblies(args)
62
+ cli_list_parts(args)
63
+
64
+ elif args.command == "list-parts":
65
+ cli_list_parts(args)
66
+
67
+ elif args.command == "list-assemblies":
68
+ cli_list_assemblies(args)
69
+
70
+ elif args.command == "render":
71
+ cli_render(args)
72
+
73
+ elif args.command == "show":
74
+ cli_show(args)
75
+
76
+ else:
77
+ print("Unknown command.\n")
78
+ parser.print_help()
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()
@@ -0,0 +1,66 @@
1
+ #
2
+ # OpenVMP, 2023
3
+ #
4
+ # Author: Roman Kuzmenko
5
+ # Created: 2023-12-23
6
+ #
7
+ # Licensed under Apache License, Version 2.0.
8
+ #
9
+
10
+ import logging
11
+ import os
12
+ import ruamel.yaml
13
+ import sys
14
+
15
+ import partcad as pc
16
+
17
+
18
+ def cli_help_add(subparsers):
19
+ parser_add = subparsers.add_parser(
20
+ "add",
21
+ help="Import a package",
22
+ )
23
+ parser_add.add_argument(
24
+ "alias",
25
+ help="Alias to be used to reference the package",
26
+ type=str,
27
+ )
28
+ parser_add.add_argument(
29
+ "location",
30
+ help="Path or URL to the package",
31
+ type=str,
32
+ )
33
+
34
+
35
+ def cli_add(args):
36
+ if not os.path.exists("partcad.yaml"):
37
+ logging.error("'partcad.yaml' not found!")
38
+ sys.exit(1)
39
+
40
+ if ":" in args.location:
41
+ location_param = "url"
42
+ if args.location.endswith(".tar.gz"):
43
+ location_type = "tar"
44
+ else:
45
+ location_type = "git"
46
+ else:
47
+ location_param = "path"
48
+ location_type = "local"
49
+
50
+ yaml = ruamel.yaml.YAML()
51
+ yaml.preserve_quotes = True
52
+ with open("partcad.yaml") as fp:
53
+ config = yaml.load(fp)
54
+ fp.close()
55
+
56
+ for elem in config:
57
+ if elem == "import":
58
+ imports = config["import"]
59
+ imports[args.alias] = {
60
+ location_param: args.location,
61
+ "type": location_type,
62
+ }
63
+ break # no need to iterate further
64
+ with open("partcad.yaml", "w") as fp:
65
+ yaml.dump(config, fp)
66
+ fp.close()
@@ -0,0 +1,49 @@
1
+ #
2
+ # OpenVMP, 2023
3
+ #
4
+ # Author: Roman Kuzmenko
5
+ # Created: 2023-12-23
6
+ #
7
+ # Licensed under Apache License, Version 2.0.
8
+ #
9
+
10
+ import logging
11
+ import os
12
+ import shutil
13
+ import sys
14
+
15
+ import partcad as pc
16
+
17
+
18
+ def cli_help_init(subparsers):
19
+ parser_init = subparsers.add_parser(
20
+ "init",
21
+ help="Initialize new PartCAD package in this directory",
22
+ )
23
+ parser_init.add_argument(
24
+ "-p",
25
+ help="Initialize this package as private",
26
+ dest="private",
27
+ action="store_true",
28
+ )
29
+
30
+
31
+ def cli_init(args):
32
+ if args.private:
33
+ template_name = "init-private.yaml"
34
+ else:
35
+ template_name = "init-public.yaml"
36
+ src_path = os.path.join(os.path.dirname(__file__), "template", template_name)
37
+
38
+ if not args.config_path is None:
39
+ if os.path.isdir(args.config_path):
40
+ dst_path = os.path.join(args.config_path, "partcad.yaml")
41
+ else:
42
+ dst_path = args.config_path
43
+ else:
44
+ dst_path = "partcad.yaml"
45
+
46
+ if os.path.exists(dst_path):
47
+ logging.error("'%s' already exists!" % dst_path)
48
+ sys.exit(1)
49
+ shutil.copyfile(src_path, dst_path)
@@ -0,0 +1,24 @@
1
+ #
2
+ # OpenVMP, 2023
3
+ #
4
+ # Author: Roman Kuzmenko
5
+ # Created: 2023-12-23
6
+ #
7
+ # Licensed under Apache License, Version 2.0.
8
+ #
9
+
10
+ import partcad as pc
11
+
12
+
13
+ def cli_help_install(subparsers):
14
+ parser_install = subparsers.add_parser(
15
+ "install",
16
+ help="Download and prepare all imported packages",
17
+ )
18
+
19
+
20
+ def cli_install(args):
21
+ if not args.config_path is None:
22
+ pc.init(args.config_path)
23
+ else:
24
+ pc.init()
@@ -0,0 +1,216 @@
1
+ #
2
+ # OpenVMP, 2023
3
+ #
4
+ # Author: Roman Kuzmenko
5
+ # Created: 2023-12-23
6
+ #
7
+ # Licensed under Apache License, Version 2.0.
8
+ #
9
+
10
+ import logging
11
+ import partcad as pc
12
+
13
+
14
+ def cli_help_list(subparsers):
15
+ parser_list = subparsers.add_parser(
16
+ "list",
17
+ help="List imported packages",
18
+ )
19
+ parser_list_all = subparsers.add_parser(
20
+ "list-all",
21
+ help="List available parts, assemblies and scenes",
22
+ )
23
+ parser_list_parts = subparsers.add_parser(
24
+ "list-parts",
25
+ help="List available parts",
26
+ )
27
+ parser_list_assemblies = subparsers.add_parser(
28
+ "list-assemblies",
29
+ help="List available assemblies",
30
+ )
31
+
32
+ parser_list_all.add_argument(
33
+ "-r",
34
+ help="Recursively process all imported packages",
35
+ dest="recursive",
36
+ action="store_true",
37
+ )
38
+ parser_list_parts.add_argument(
39
+ "-r",
40
+ help="Recursively process all imported packages",
41
+ dest="recursive",
42
+ action="store_true",
43
+ )
44
+ parser_list_assemblies.add_argument(
45
+ "-r",
46
+ help="Recursively process all imported packages",
47
+ dest="recursive",
48
+ action="store_true",
49
+ )
50
+
51
+ parser_list_all.add_argument(
52
+ "-u",
53
+ help="Only process objects used by the given assembly or scene.",
54
+ dest="used_by",
55
+ type=str,
56
+ required=False,
57
+ )
58
+ parser_list_parts.add_argument(
59
+ "-u",
60
+ help="Only process objects used by the given assembly or scene.",
61
+ dest="used_by",
62
+ type=str,
63
+ required=False,
64
+ )
65
+ parser_list_assemblies.add_argument(
66
+ "-u",
67
+ help="Only process objects used by the given assembly or scene.",
68
+ dest="used_by",
69
+ type=str,
70
+ required=False,
71
+ )
72
+
73
+
74
+ def cli_list(args):
75
+ if not args.config_path is None:
76
+ ctx = pc.init(args.config_path)
77
+ else:
78
+ ctx = pc.init()
79
+ pkg_count = 0
80
+
81
+ print()
82
+ print("PartCAD dependencies:")
83
+ for project_name in ctx.projects:
84
+ if project_name == pc.THIS or project_name.startswith("partcad-"):
85
+ continue
86
+
87
+ project = ctx.projects[project_name]
88
+ line = "\t%s" % project_name
89
+ padding_size = 40 - len(project_name)
90
+ if padding_size < 4:
91
+ padding_size = 4
92
+ line += " " * padding_size
93
+ line += "%s" % project.desc
94
+ print(line)
95
+ pkg_count = pkg_count + 1
96
+
97
+ if pkg_count < 1:
98
+ print("\t<none>")
99
+
100
+
101
+ def cli_list_parts(args):
102
+ if not args.config_path is None:
103
+ ctx = pc.init(args.config_path)
104
+ else:
105
+ ctx = pc.init()
106
+ part_count = 0
107
+ part_kinds = 0
108
+
109
+ if args.used_by is not None:
110
+ print("Instantiating %s..." % args.used_by)
111
+ pc.get_assembly(args.used_by)
112
+
113
+ print()
114
+ print("PartCAD parts:")
115
+ for project_name in ctx.projects:
116
+ if project_name != pc.THIS and not args.recursive:
117
+ continue
118
+ if project_name.startswith("partcad-"):
119
+ continue
120
+ project = ctx.projects[project_name]
121
+
122
+ if args.used_by is None:
123
+ if (
124
+ "parts" in project.config_obj
125
+ and project.config_obj["parts"] is not None
126
+ ):
127
+ part_names = project.config_obj["parts"].keys()
128
+ else:
129
+ part_names = []
130
+ else:
131
+ part_names = project.parts.keys()
132
+ for part_name in part_names:
133
+ line = "\t"
134
+ if args.recursive:
135
+ line += "%s" % project_name
136
+ line += " " * (36 - len(project_name))
137
+ line += "%s" % part_name
138
+ if args.used_by is not None:
139
+ part = project.parts[part_name]
140
+ line += "(%d)" % part.count
141
+ part_count = part_count + part.count
142
+ line += " " * (36 - len(part_name))
143
+ if (
144
+ project.config_obj["parts"][part_name] is not None
145
+ and "desc" in project.config_obj["parts"][part_name]
146
+ ):
147
+ line += "%s" % project.config_obj["parts"][part_name]["desc"]
148
+ print(line)
149
+ part_kinds = part_kinds + 1
150
+
151
+ if part_kinds > 0:
152
+ if args.used_by is None:
153
+ print("Total: %d" % part_kinds)
154
+ else:
155
+ print("Total: %d parts of %d kinds" % (part_count, part_kinds))
156
+ else:
157
+ print("\t<none>")
158
+
159
+
160
+ def cli_list_assemblies(args):
161
+ if not args.config_path is None:
162
+ ctx = pc.init(args.config_path)
163
+ else:
164
+ ctx = pc.init()
165
+ assy_count = 0
166
+ assy_kinds = 0
167
+
168
+ if args.used_by is not None:
169
+ print("Instantiating %s..." % args.used_by)
170
+ pc.get_assembly(args.used_by)
171
+
172
+ print()
173
+ print("PartCAD assemblies:")
174
+ for project_name in ctx.projects:
175
+ if project_name != pc.THIS and not args.recursive:
176
+ continue
177
+ if project_name.startswith("partcad-"):
178
+ continue
179
+ project = ctx.projects[project_name]
180
+
181
+ if args.used_by is None:
182
+ if (
183
+ "assemblies" in project.config_obj
184
+ and project.config_obj["assemblies"] is not None
185
+ ):
186
+ assy_names = project.config_obj["assemblies"].keys()
187
+ else:
188
+ assy_names = []
189
+ else:
190
+ assy_names = project.assemblies.keys()
191
+ for assy_name in assy_names:
192
+ line = "\t"
193
+ if args.recursive:
194
+ line += "%s" % project_name
195
+ line += " " * (36 - len(project_name))
196
+ line += "%s" % assy_name
197
+ if args.used_by is not None:
198
+ assy = project.assemblies[assy_name]
199
+ line += "(%d)" % assy.count
200
+ assy_count = assy_count + assy.count
201
+ line += " " * (36 - len(assy_name))
202
+ if (
203
+ project.config_obj["assemblies"][assy_name] is not None
204
+ and "desc" in project.config_obj["assemblies"][assy_name]
205
+ ):
206
+ line += "%s" % project.config_obj["assemblies"][assy_name]["desc"]
207
+ print(line)
208
+ assy_kinds = assy_kinds + 1
209
+
210
+ if assy_kinds > 0:
211
+ if args.used_by is None:
212
+ print("Total: %d" % assy_kinds)
213
+ else:
214
+ print("Total: %d assemblies of %d kinds" % (assy_count, assy_kinds))
215
+ else:
216
+ print("\t<none>")
@@ -0,0 +1,76 @@
1
+ #
2
+ # OpenVMP, 2023-2024
3
+ #
4
+ # Author: Roman Kuzmenko
5
+ # Created: 2023-12-23
6
+ #
7
+ # Licensed under Apache License, Version 2.0.
8
+ #
9
+
10
+ import argparse
11
+
12
+ import partcad as pc
13
+
14
+
15
+ def cli_help_render(subparsers: argparse.ArgumentParser):
16
+ parser_render = subparsers.add_parser(
17
+ "render",
18
+ help="Render the selected or all parts, assemblies and scenes in this package",
19
+ )
20
+ group_type = parser_render.add_mutually_exclusive_group(required=False)
21
+ group_type.add_argument(
22
+ "-a",
23
+ help="The object is an assembly",
24
+ dest="assembly",
25
+ action="store_true",
26
+ )
27
+ group_type.add_argument(
28
+ "-s",
29
+ help="The object is a scene",
30
+ dest="scene",
31
+ action="store_true",
32
+ )
33
+
34
+ parser_render.add_argument(
35
+ "object",
36
+ help="Part (default), assembly or scene to render",
37
+ type=str,
38
+ nargs="?",
39
+ )
40
+ # TODO(clairbee): make the package argument depending on the object argument
41
+ # [<object> [<package>]]
42
+ # instead of
43
+ # [<object>] [<package>]
44
+ parser_render.add_argument(
45
+ "package",
46
+ help="Package to retrieve the object from",
47
+ type=str,
48
+ nargs="?",
49
+ )
50
+
51
+
52
+ def cli_render(args):
53
+ if not args.config_path is None:
54
+ ctx = pc.init(args.config_path)
55
+ else:
56
+ ctx = pc.init()
57
+
58
+ if args.package is None:
59
+ package = "this"
60
+ else:
61
+ package = args.package
62
+
63
+ if args.object is None:
64
+ # Render all parts and assemblies configured to be auto-rendered in this project
65
+ ctx.render()
66
+ else:
67
+ # Render the requested part or assembly
68
+ parts = []
69
+ assemblies = []
70
+ if args.assembly:
71
+ assemblies.append(args.object)
72
+ else:
73
+ parts.append(args.object)
74
+
75
+ prj = ctx.get_project(package)
76
+ prj.render(parts=parts, assemblies=assemblies)
@@ -0,0 +1,71 @@
1
+ #
2
+ # OpenVMP, 2023
3
+ #
4
+ # Author: Roman Kuzmenko
5
+ # Created: 2023-12-23
6
+ #
7
+ # Licensed under Apache License, Version 2.0.
8
+ #
9
+
10
+ import logging
11
+
12
+ import partcad as pc
13
+
14
+
15
+ def cli_help_show(subparsers):
16
+ parser_show = subparsers.add_parser(
17
+ "show",
18
+ help="Visualize a part, assembly or scene",
19
+ )
20
+ group_type = parser_show.add_mutually_exclusive_group(required=False)
21
+ group_type.add_argument(
22
+ "-a",
23
+ help="The object is an assembly",
24
+ dest="assembly",
25
+ action="store_true",
26
+ )
27
+ group_type.add_argument(
28
+ "-s",
29
+ help="The object is a scene",
30
+ dest="scene",
31
+ action="store_true",
32
+ )
33
+
34
+ parser_show.add_argument(
35
+ "object",
36
+ help="Part (default), assembly or scene to show",
37
+ type=str,
38
+ )
39
+ parser_show.add_argument(
40
+ "package",
41
+ help="Package to retrieve the object from",
42
+ type=str,
43
+ nargs="?",
44
+ )
45
+
46
+
47
+ def cli_show(args):
48
+ if not args.config_path is None:
49
+ ctx = pc.init(args.config_path)
50
+ else:
51
+ ctx = pc.init()
52
+
53
+ if args.package is None:
54
+ package = "this"
55
+ else:
56
+ package = args.package
57
+
58
+ if args.assembly:
59
+ obj = ctx.get_assembly(args.object, package)
60
+ else:
61
+ obj = ctx.get_part(args.object, package)
62
+
63
+ if obj is None:
64
+ if args.package is None:
65
+ logging.error("Object %s not found" % args.object)
66
+ else:
67
+ logging.error(
68
+ "Object %s not found in package %s" % (args.object, args.package)
69
+ )
70
+ else:
71
+ obj.show()
@@ -0,0 +1,16 @@
1
+ import:
2
+ # Add dependencies here
3
+ # <package-name>:
4
+ # desc: <package description> # optional
5
+ # type: <local|git|tar>
6
+ # path: <relative path> # type:local only
7
+ # url: <url of the package> # type:git|tar only
8
+ # relPath: <relative path within the repo> # type:git|tar only
9
+ # web: <package's or maintainer's url> # optional
10
+ # poc: <maintainer's email> # optional
11
+
12
+ parts:
13
+ # Add parts here
14
+
15
+ assemblies:
16
+ # Add assemblies here
@@ -0,0 +1,19 @@
1
+ import:
2
+ # Add dependencies here
3
+ # <package-name>:
4
+ # desc: <package description> # optional
5
+ # type: <local|git|tar>
6
+ # path: <relative path> # type:local only
7
+ # url: <url of the package> # type:git|tar only
8
+ # relPath: <relative path within the repo> # type:git|tar only
9
+ # web: <package's or maintainer's url> # optional
10
+ # poc: <maintainer's email> # optional
11
+ partcad-index:
12
+ type: git
13
+ url: https://github.com/openvmp/partcad-index.git
14
+
15
+ parts:
16
+ # Add parts here
17
+
18
+ assemblies:
19
+ # Add assemblies here
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: partcad-cli
3
+ Version: 0.3.1
4
+ Summary: Command-line interface to PartCAD
5
+ Author-email: Roman Kuzmenko <openvmp@proton.me>
6
+ Maintainer-email: Roman Kuzmenko <openvmp@proton.me>
7
+ Project-URL: Homepage, https://github.com/openvmp/partcad
8
+ Project-URL: Issues, https://github.com/openvmp/partcad/issues
9
+ Keywords: cadquery,build123d,cad,design,openscad,step,stl
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: partcad==0.3.1
16
+ Requires-Dist: pycairo
17
+ Requires-Dist: renderlab
18
+ Requires-Dist: rlPyCairo
19
+ Requires-Dist: svglib
20
+ Requires-Dist: ocp_vscode
21
+ Requires-Dist: progress
@@ -0,0 +1,18 @@
1
+ pyproject.toml
2
+ requirements.txt
3
+ src/partcad_cli/__init__.py
4
+ src/partcad_cli/cli.py
5
+ src/partcad_cli/cli_add.py
6
+ src/partcad_cli/cli_init.py
7
+ src/partcad_cli/cli_install.py
8
+ src/partcad_cli/cli_list.py
9
+ src/partcad_cli/cli_render.py
10
+ src/partcad_cli/cli_show.py
11
+ src/partcad_cli.egg-info/PKG-INFO
12
+ src/partcad_cli.egg-info/SOURCES.txt
13
+ src/partcad_cli.egg-info/dependency_links.txt
14
+ src/partcad_cli.egg-info/entry_points.txt
15
+ src/partcad_cli.egg-info/requires.txt
16
+ src/partcad_cli.egg-info/top_level.txt
17
+ src/partcad_cli/template/init-private.yaml
18
+ src/partcad_cli/template/init-public.yaml
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pc = partcad_cli:main_cli
@@ -0,0 +1,7 @@
1
+ partcad==0.3.1
2
+ pycairo
3
+ renderlab
4
+ rlPyCairo
5
+ svglib
6
+ ocp_vscode
7
+ progress
@@ -0,0 +1 @@
1
+ partcad_cli