partomatic 0.0.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.
Binary file
@@ -0,0 +1,163 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
161
+
162
+ #VS Code Counter
163
+ .VSCodeCounter/
@@ -0,0 +1,7 @@
1
+ {
2
+ "python.testing.pytestArgs": [
3
+ "tests"
4
+ ],
5
+ "python.testing.unittestEnabled": false,
6
+ "python.testing.pytestEnabled": true
7
+ }
@@ -0,0 +1,7 @@
1
+ Copyright 2024 Christopher Litsinger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: partomatic
3
+ Version: 0.0.1
4
+ Summary: build123d Part extended for CI/CD automation
5
+ Project-URL: Homepage, https://github.com/x0pher/partomatic
6
+ Project-URL: Issues, https://github.com/x0pher/partomatic/issues
7
+ Project-URL: docs, https://partomatic.readthedocs.org
8
+ Project-URL: documentation, https://partomatic.readthedocs.org
9
+ Author: x0pherl
10
+ License-File: LICENSE
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Partomatic
18
+
19
+ Partomatic is an attempt to build an automatable ecosystem for generating parametric models through automation -- making CI/CD automation possible for your 3d models.
20
+
21
+ # The Partomatic philosophy
22
+
23
+ Build123d is a powerful library, but it leaves the creation of final parts up to the developer. For a large project with many related and interlocking parts, this can make releasing a new version a project in and of itself.
24
+
25
+ [Partomatic](https://github.com/x0pherl/partomatic) enables _parametric modeling_ and standardizes some _build automation_ for a part.
26
+
27
+ ## Parametric Modeling
28
+ Parametric 3D modeling is a method of creating 3D models where the geometry is defined by parameters, allowing for easy adjustment by simply changing the values of these parameters. This approach enables the creation of flexible and reusable designs that can be quickly adapted to different requirements.
29
+
30
+ ## Build Automation
31
+
32
+ Build automation is a common practice in the software delivery world. Continuous Integration uses build automation to deliver software into testing and production environments whenever changes are checked in by a developer. Partomatic wraps additional information about how to name files and where to store them, so that an automated build script generate and save those parts.
@@ -0,0 +1,16 @@
1
+ # Partomatic
2
+
3
+ Partomatic is an attempt to build an automatable ecosystem for generating parametric models through automation -- making CI/CD automation possible for your 3d models.
4
+
5
+ # The Partomatic philosophy
6
+
7
+ Build123d is a powerful library, but it leaves the creation of final parts up to the developer. For a large project with many related and interlocking parts, this can make releasing a new version a project in and of itself.
8
+
9
+ [Partomatic](https://github.com/x0pherl/partomatic) enables _parametric modeling_ and standardizes some _build automation_ for a part.
10
+
11
+ ## Parametric Modeling
12
+ Parametric 3D modeling is a method of creating 3D models where the geometry is defined by parameters, allowing for easy adjustment by simply changing the values of these parameters. This approach enables the creation of flexible and reusable designs that can be quickly adapted to different requirements.
13
+
14
+ ## Build Automation
15
+
16
+ Build automation is a common practice in the software delivery world. Continuous Integration uses build automation to deliver software into testing and production environments whenever changes are checked in by a developer. Partomatic wraps additional information about how to name files and where to store them, so that an automated build script generate and save those parts.
@@ -0,0 +1,20 @@
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@@ -0,0 +1,124 @@
1
+ # Configuration file for the Sphinx documentation builder.
2
+ #
3
+ # This file only contains a selection of the most common options. For a full
4
+ # list see the documentation:
5
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
6
+
7
+ # -- Path setup --------------------------------------------------------------
8
+
9
+ # If extensions (or modules to document with autodoc) are in another directory,
10
+ # add these directories to sys.path here. If the directory is relative to the
11
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
12
+ #
13
+ import os
14
+ import sys
15
+
16
+ partomatic_path = os.path.dirname(os.path.abspath(os.getcwd()))
17
+ source_files_path = os.path.join(partomatic_path, "src", "partomatic")
18
+ sys.path.insert(0, source_files_path)
19
+ sys.path.append(os.path.abspath("sphinxext"))
20
+ sys.path.insert(0, os.path.abspath("."))
21
+ sys.path.insert(0, os.path.abspath("../"))
22
+
23
+ # -- Project information -----------------------------------------------------
24
+
25
+ project = "partomatic"
26
+ copyright = "2024, x0pherl"
27
+ author = "x0pherl"
28
+
29
+ # The full version, including alpha/beta/rc tags
30
+ release = "latest"
31
+
32
+
33
+ # -- General configuration ---------------------------------------------------
34
+
35
+ # Add any Sphinx extension module names here, as strings. They can be
36
+ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
37
+ # ones.
38
+ extensions = [
39
+ "sphinx.ext.napoleon",
40
+ "sphinx.ext.autodoc",
41
+ # "sphinx_autodoc_typehints",
42
+ "sphinx.ext.autodoc.typehints",
43
+ "sphinx.ext.doctest",
44
+ "sphinx.ext.graphviz",
45
+ "sphinx.ext.inheritance_diagram",
46
+ "sphinx.ext.viewcode",
47
+ "sphinx_design",
48
+ "sphinx_copybutton",
49
+ "hoverxref.extension",
50
+ ]
51
+
52
+ # Napoleon settings
53
+ napoleon_google_docstring = True
54
+ napoleon_numpy_docstring = True
55
+ napoleon_include_init_with_doc = False
56
+ napoleon_include_private_with_doc = False
57
+ napoleon_include_special_with_doc = False
58
+ napoleon_use_admonition_for_examples = False
59
+ napoleon_use_admonition_for_notes = False
60
+ napoleon_use_admonition_for_references = False
61
+ napoleon_use_ivar = True
62
+ napoleon_use_param = True
63
+ napoleon_use_rtype = True
64
+ napoleon_use_keyword = True
65
+ napoleon_custom_sections = None
66
+
67
+ autodoc_typehints = ["signature"]
68
+ # autodoc_typehints = ["description"]
69
+ # autodoc_typehints = ["both"]
70
+
71
+ autodoc_default_options = {
72
+ "members": True,
73
+ "undoc-members": True,
74
+ "member-order": "alphabetical",
75
+ "show-inheriance": False,
76
+ }
77
+
78
+ # Sphinx settings
79
+ add_module_names = False
80
+ python_use_unqualified_type_names = True
81
+
82
+ # Add any paths that contain templates here, relative to this directory.
83
+ templates_path = ["_templates"]
84
+
85
+ # List of patterns, relative to source directory, that match files and
86
+ # directories to ignore when looking for source files.
87
+ # This pattern also affects html_static_path and html_extra_path.
88
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
89
+
90
+
91
+ # -- Options for HTML output -------------------------------------------------
92
+
93
+ # The theme to use for HTML and HTML Help pages. See the documentation for
94
+ # a list of builtin themes.
95
+ #
96
+ # html_theme = "alabaster"
97
+ html_theme = "sphinx_rtd_theme"
98
+
99
+ # Add any paths that contain custom static files (such as style sheets) here,
100
+ # relative to this directory. They are copied after the builtin static files,
101
+ # so a file named "default.css" will overwrite the builtin "default.css".
102
+ html_static_path = ["_static"]
103
+
104
+ # -- Options for hoverxref -------------------------------------------------
105
+ hoverxref_role_types = {
106
+ "hoverxref": "tooltip",
107
+ "ref": "tooltip", # for hoverxref_auto_ref config
108
+ "confval": "tooltip", # for custom object
109
+ "mod": "tooltip", # for Python Sphinx Domain
110
+ "class": "tooltip", # for Python Sphinx Domain
111
+ "meth": "tooltip", # for Python Sphinx Domain
112
+ "func": "tooltip", # for Python Sphinx Domain
113
+ }
114
+
115
+ hoverxref_roles = [
116
+ "class",
117
+ "meth",
118
+ ]
119
+
120
+ hoverxref_domains = [
121
+ "py",
122
+ ]
123
+
124
+ html_logo = "assets/logo.svg"
@@ -0,0 +1 @@
1
+ {!README.md!}
@@ -0,0 +1,225 @@
1
+ ## The elements of Partomatic
2
+
3
+ There are three elements of Partomatic.
4
+ - `BuildablePart`
5
+ - `PartomaticConfig`
6
+ - `Partomatic`
7
+
8
+ ## BuildablePart
9
+
10
+ BuildablePart is a small wrapper around build123d's Part class that adds some useful additional data for generating parts in an automated context. These variables are members of the BuildablePart class:]
11
+ ```
12
+ part: Part = field(default_factory=Part)
13
+ display_location: Location = field(default_factory=Location)
14
+ stl_folder: str = getcwd()
15
+ _file_name: str = "partomatic"
16
+ ```
17
+
18
+ `part` is simply a build123d `Part` object
19
+ `display_location` defines a build123d `Location` in which to display the object (this is useful combining multiple `BuildablePart`s into a single Partomatic object, and will be covered below)
20
+ `stl_folder` defines the folder in which the part should be saved
21
+ `file_name` (there are getters and setters for the `_filename` variable) defines the base file name. Note that this base will likely be combined with prefixes and suffixes that describe the parametric configuration, so any extension that is passed will be automatically stripped off.
22
+
23
+ ## PartomaticConfig
24
+
25
+ The first element of Partomatic is the PartomaticConfig class. Descending a class from PartomaticConfig allows you to define any parametric values for your design.
26
+
27
+ PartomaticConfig makes it easy to load parametric values from Python parameters passed on instantiation, or through a YAML file -- you can even nest PartomaticConfig object definitions in a single YAML file.
28
+
29
+ YAML was chosen because YAML files are easily human-readable without deep technical knowledge. As an example, imagine a simple model of a wheel with a cut in the center for a bearing. We'll define both the wheel and the bearing. A simple example of a YAML configuration for a wheel with a bearing axle might look like:
30
+
31
+ ```
32
+ wheel:
33
+ depth: 10
34
+ radius: 30
35
+ bearing:
36
+ radius: 4
37
+ spindle_radius: 1.5
38
+ ```
39
+ Now we can define PartomaticConfig objects for both the Wheel and the Bearing as follows:
40
+
41
+ ```
42
+ from partomatic import PartomaticConfig
43
+ from dataclasses import field
44
+
45
+ class BearingConfig(PartomaticConfig):
46
+ yaml_tree: str = "wheel/bearing"
47
+ radius: float = 10
48
+ spindle_radius: float = 2
49
+
50
+ class WheelConfig(PartomaticConfig):
51
+ yaml_tree = "wheel"
52
+ depth: float = 2
53
+ radius: float = 50
54
+ bearing: BearingConfig = field(default_factory=BearingConfig)
55
+ ```
56
+
57
+ You may have noted a few things that aren't obvious given the YAML section above. Let's take a deeper look at yaml_tree and the field definition for the bearing.
58
+
59
+ ### yaml_tree
60
+
61
+ The value `yaml_tree` defines the tree of the configuration within a file that you would like to load. For our example, not that "wheel" is the root object of our yaml file because our first line reads `wheel:`.
62
+
63
+ Bearing is a sub element of that wheel object, because it is at the same indent level as `depth` and `radius`. Partomatic separates objects on the tree with the `/` character, so we define the bearing's `yaml_tree` as `wheel/bearing` so it could be loaded independently from the same file.
64
+
65
+ Note that the yaml tree of the sub object is not _required_ to follow this pattern. In our sample case it makes it easy to load a bearing object from the same file as the wheel if only the bearing is required for some python files within our project. `yaml_tree` can also be passed when initializing the BearingConfig object, so it could be overwritten if appropriate.
66
+
67
+ ### field factory
68
+
69
+ Field factory functions are beyond the scope of this documentation, however the [*dataclass* documentation](https://docs.python.org/3/library/dataclasses.html#default-factory-functions) covers this thoroughly.
70
+
71
+ For PartomaticConfig, all you need to understand is that if you are nesting PartomaticConfig objects, you should follow this pattern when adding the sub-object to the base part:
72
+ `<object_name>: <ObjectClass> = field(default_factory=<ObjectClass>)`
73
+ Have a look again at the bearing field of the `WheelConfig` object for an example.
74
+
75
+ ### Instantiating a PartomaticConfig descendant
76
+
77
+ Now that we've got the `WheelConfig` (and it's member class `BearingConfig`) defined we need to create an instance of `WheelConfig`. We can instantiate this in several ways:
78
+ - default configuration
79
+ - loading from a file
80
+ - loading from a yaml string
81
+ - defining parameters
82
+
83
+ #### Instantiating with default configuration
84
+
85
+ If you're happy with the default values for your wheel configuration (and its bearing), it couldn't be simpler to instantiate:
86
+ `wheel_config = WheelConfig()`
87
+
88
+ #### Instantiating by loading from a file
89
+
90
+ Loading from a yaml file can make it easy to build multiple parts with different configurations.
91
+
92
+ In our example, you might define multiple wheel parts to support different bearings sizes and add prefixes with the standard bearing names. Each of these configurations can be defined in a separate file, and we can use automation to process each of them.
93
+
94
+ Instantiating a Partomatic object from a yaml file is as simple as passing a filename to a valid yaml file as the only parameter:
95
+ `wheel_config = WheelConfig('~/wheel/config/base_wheel.yml')`
96
+
97
+
98
+ #### Instantiating with a yaml string
99
+
100
+ If you've loaded a yaml string out of another object or from an environment variable, you can pass the entire yaml string instead of a filename as shown in this example:
101
+ ```
102
+ wheel_yaml = """
103
+ wheel:
104
+ depth: 10
105
+ radius: 30
106
+ bearing:
107
+ radius: 4
108
+ spindle_radius: 1.5
109
+ """
110
+ wheel_config = WheelConfig(wheel_yaml)
111
+ ```
112
+
113
+ Remember that you can also load the object from anywhere in a `yaml_tree`; so if the `wheel` object is defined in a yaml tree for a parent object you could use that as follows:
114
+
115
+ ```
116
+ car_yaml = """
117
+ car:
118
+ <some car values>
119
+ drivetrain:
120
+ <some drivetrain values>
121
+ wheel:
122
+ depth: 10
123
+ radius: 30
124
+ bearing:
125
+ radius: 4
126
+ spindle_radius: 1.5
127
+ """
128
+ wheel_config = WheelConfig(car_yaml, yaml_tree='car/drivetrain/wheel')
129
+ ```
130
+
131
+ #### Instantiating with parameters passed
132
+
133
+ If you understand the correct parameters from elsewhere in your code, you could simply define each of those as kwargs and pass them to the definition as in this example:
134
+
135
+ ```
136
+ bearing_config = BearingConfig(radius=20, spindle_radius=10)
137
+ wheel_config = WheelConfig(depth=5, radius=50, bearing=bearing_config)
138
+ ```
139
+
140
+ ### Other PartomaticConfig fields
141
+
142
+ The base PartomaticConfig object also declares the following fields:
143
+ ```
144
+ stl_folder: str = "NONE"
145
+ file_prefix: str = ""
146
+ file_suffix: str = ""
147
+ create_folders_if_missing: bool = True
148
+ ```
149
+
150
+ #### `stl_folder`
151
+ This defines the folder in which Partomatic STL files will be generated
152
+
153
+ #### `file_prefix`
154
+ Your `Partomatic` object will generate one or more parts, and it defines file names for each part. The `file_prefix` allows you to define a prefix that will be added to each file when saving. This makes it possible to generate parts from multiple configurations in the same folder.
155
+
156
+ In our example, where we are defining multiple wheel parts to support different bearings sizes, we might add prefixes with the standard bearing names.
157
+
158
+ #### `file_suffix`
159
+ This works the same way as `file_prefix` (described above), but adds this string to the end of each generated file.
160
+
161
+ #### `create_folders_if_missing`
162
+ By default, Partomatic will create folders if they don't exist when exporting stl files. If you prefer it to only save parts if the folders already exist, you set this to `False`
163
+
164
+ ## Partomatic
165
+
166
+ Partomatic is an [abstract base class](https://docs.python.org/3/library/abc.html) for components within a larger project.
167
+
168
+ Partomatic automatically handles the `__init__` method as well as `load_config`. Overriding these methods is not recommended.
169
+
170
+ ### Defined Partomatic Variables
171
+
172
+ Partomatic defines two important variables that you descendent classes will inherit:
173
+ ```
174
+ _config: PartomaticConfig
175
+ parts: list[BuildablePart] = field(default_factory=list)
176
+ ```
177
+
178
+ `_config` stores the parameters from a PartomaticConfig object. `parts` is a list of BuildableParts, which partomatic will display or export when the appropriate methods are called.
179
+
180
+ ### Abstract `compile` method
181
+
182
+ Partomatic defines an abstract methods which must be defined within a descendent class.
183
+
184
+ This method is responsible for generating the 3d geometry for each component. It should clear the parts list and regenerate each element of your design as a BuildablePart.
185
+
186
+ A simple example might look like this:
187
+
188
+ ```
189
+ # ... Partomatic descendant class fragment
190
+
191
+ def complete_wheel() -> Part:
192
+ # <CODE TO GENERATE PART>
193
+
194
+ def compile(self):
195
+ """
196
+ Builds the relevant parts for the filament wheel
197
+ """
198
+ self.parts.clear()
199
+ self.parts.append(
200
+ BuildablePart(
201
+ self.complete_wheel(),
202
+ "complete-wheel",
203
+ stl_folder=self._config.stl_folder,
204
+ )
205
+ )
206
+
207
+ ```
208
+
209
+ ### Partomatic built-in methods
210
+
211
+ #### `display`
212
+
213
+ The `display` method will display each BuildablePart in the `parts` list in the appropriate display_location
214
+
215
+ #### `export_stls`
216
+
217
+ This method calculates the appropriate file path based on the descendant class' `stl_folder`, `file_prefix`, the `BuildablePart`'s `file_name` and the `file_prefix`. If `create_folders_if_missing` is set to False, no part will be saved if the file is not present.
218
+
219
+ #### `load_config`
220
+
221
+ This method will load a configuration from file, kwargs, or a yaml string -- see the `PartomaticConfig` documentation for more details.
222
+
223
+ #### `partomate`
224
+
225
+ `partomate` is a convenience function that will execute the `compile` and `export_stls` functions of the Partomatic descendant.
@@ -0,0 +1,3 @@
1
+ mkdocs
2
+ mkdocs-material
3
+ markdown-include
@@ -0,0 +1,20 @@
1
+ site_name: Partomatic
2
+ site_url: https://partomatic.readthedocs.io/
3
+ theme:
4
+ name: material
5
+ features:
6
+ - navigation.instant
7
+ - navigation.expand
8
+ - navigation.path
9
+ - toc.integrate
10
+ markdown_extensions:
11
+ - attr_list
12
+ - pymdownx.emoji:
13
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
14
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
15
+ - toc:
16
+ toc_depth: 3
17
+ - markdown_include.include:
18
+ base_path: .
19
+ nav:
20
+ - Developer’s Guide: partomatic.md
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "partomatic"
3
+ version = "0.0.1"
4
+ authors = [
5
+ { name="x0pherl"},
6
+ ]
7
+ description = "build123d Part extended for CI/CD automation"
8
+ readme = "README.md"
9
+ requires-python = ">=3.8"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/x0pher/partomatic"
18
+ Issues = "https://github.com/x0pher/partomatic/issues"
19
+ docs = "https://partomatic.readthedocs.org"
20
+ documentation = "https://partomatic.readthedocs.org"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
@@ -0,0 +1,13 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: "3"
7
+
8
+ python:
9
+ install:
10
+ - requirements: docs/requirements.txt
11
+
12
+ mkdocs:
13
+ configuration: mkdocs.yml
@@ -0,0 +1,4 @@
1
+ build123d
2
+ yaml
3
+ pytest
4
+ unittest
@@ -0,0 +1,3 @@
1
+ from .partomatic import Partomatic
2
+ from .buildable_part import BuildablePart
3
+ from .partomatic_config import PartomaticConfig
@@ -0,0 +1,39 @@
1
+ """BuildablePart is a dataclass that contains a Part object and additional inormation for saving and
2
+ displaying the part"""
3
+
4
+ from dataclasses import dataclass, field, fields, is_dataclass, MISSING
5
+ from pathlib import Path
6
+ from os import getcwd
7
+
8
+ from build123d import Part, Location
9
+
10
+
11
+ @dataclass
12
+ class BuildablePart(Part):
13
+ part: Part = field(default_factory=Part)
14
+ display_location: Location = field(default_factory=Location)
15
+ stl_folder: str = getcwd()
16
+ _file_name: str = "partomatic"
17
+
18
+ def __init__(self, part, file_name, **kwargs):
19
+ self.display_location = Location()
20
+ self.file_name = file_name
21
+ self.part = part
22
+ if "display_location" in kwargs:
23
+ display_location = kwargs["display_location"]
24
+ if isinstance(display_location, Location):
25
+ self.display_location = display_location
26
+ if "stl_folder" in kwargs:
27
+ self.stl_folder = kwargs["stl_folder"]
28
+
29
+ @property
30
+ def file_name(self) -> str:
31
+ return self._file_name
32
+
33
+ @file_name.setter
34
+ def file_name(self, value: str):
35
+ """
36
+ Assigns the file name to the BuildablePart, ensuring that no
37
+ file extension is included.
38
+ """
39
+ self._file_name = Path(value).stem
@@ -0,0 +1,124 @@
1
+ """Part extended for CI/CD automation"""
2
+
3
+ from dataclasses import dataclass, field, fields, is_dataclass, MISSING
4
+ from abc import ABC, abstractmethod
5
+ from pathlib import Path
6
+
7
+ from build123d import Part, Location, export_stl
8
+
9
+ import ocp_vscode
10
+
11
+ import yaml
12
+
13
+ from .partomatic_config import PartomaticConfig
14
+ from .buildable_part import BuildablePart
15
+
16
+
17
+ class Partomatic(ABC):
18
+ """
19
+ Partomatic is an extension of the Compound class from build123d
20
+ that allows for automation within a continuous integration
21
+ environment. Descendant classes must implement:
22
+ - compile: generating the geometry of components in the parts list
23
+ """
24
+
25
+ _config: PartomaticConfig
26
+ parts: list[BuildablePart] = field(default_factory=list)
27
+
28
+ @abstractmethod
29
+ def compile(self):
30
+ """
31
+ Builds the relevant parts for the partomatic part
32
+ """
33
+
34
+ def display(self):
35
+ """
36
+ Shows the relevant parts in OCP CAD Viewer
37
+ """
38
+ ocp_vscode.show(
39
+ (
40
+ [
41
+ part.part.move(Location(part.display_location))
42
+ for part in self.parts
43
+ ]
44
+ ),
45
+ reset_camera=ocp_vscode.Camera.KEEP,
46
+ )
47
+
48
+ def complete_stl_file_path(self, part: BuildablePart) -> str:
49
+ return str(
50
+ Path(
51
+ Path(part.stl_folder)
52
+ / f"{self._config.file_prefix}{part.file_name}{self._config.file_suffix}"
53
+ ).with_suffix(".stl")
54
+ )
55
+
56
+ def export_stls(self):
57
+ """
58
+ Generates the relevant STLs in the configured
59
+ folder
60
+ """
61
+ if self._config.stl_folder == "NONE":
62
+ return
63
+ for part in self.parts:
64
+ Path(self.complete_stl_file_path(part)).parent.mkdir(
65
+ parents=True, exist_ok=self._config.create_folders_if_missing
66
+ )
67
+ if (
68
+ not Path(self.complete_stl_file_path(part)).parent.exists()
69
+ or not Path(self.complete_stl_file_path(part)).parent.is_dir()
70
+ ):
71
+ raise FileNotFoundError(
72
+ f"Directory {Path(self.complete_stl_file_path(part)).parent} does not exist"
73
+ )
74
+ export_stl(part.part, self.complete_stl_file_path(part))
75
+
76
+ def load_config(self, configuration: any, **kwargs):
77
+ """
78
+ loads a partomatic configuration from a file or valid yaml
79
+ -------
80
+ arguments:
81
+ - configuration: the path to the configuration file
82
+ OR
83
+ a valid yaml configuration string
84
+ -------
85
+ notes:
86
+ if yaml_tree is set in the PartomaticConfig descendent,
87
+ PartomaticConfig will use that tree to find a node deep
88
+ within the yaml tree, following the node names separated by slashes
89
+ (example: "BigObject/Partomatic")
90
+ """
91
+ self._config.load_config(configuration, **kwargs)
92
+
93
+ def __init__(self, configuration: any = None, **kwargs):
94
+ """
95
+ loads a partomatic configuration from a file or valid yaml
96
+ -------
97
+ arguments:
98
+ - configuration: the path to the configuration file
99
+ OR
100
+ a valid yaml configuration string
101
+ OR
102
+ None (default) for an empty object
103
+ - **kwargs: specific fields to set in the configuration
104
+ -------
105
+ notes:
106
+ you can assign yaml_tree as a kwarg here to load a
107
+ configuration from a node node deep within the yaml tree,
108
+ following the node names separated by slashes
109
+ (example: "BigObject/Partomatic")
110
+ """
111
+ self.parts = []
112
+ self._config = self.__class__._config
113
+ self.load_config(configuration, **kwargs)
114
+
115
+ def partomate(self):
116
+ """automates the part generation and exports stl and step models
117
+ -------
118
+ notes:
119
+ - if you want to avoid exporting one of those file formats,
120
+ you can override the export_stls or export_steps methods
121
+ with a no-op method using the pass keyword
122
+ """
123
+ self.compile()
124
+ self.export_stls()
@@ -0,0 +1,131 @@
1
+ """Part extended for CI/CD automation"""
2
+
3
+ from dataclasses import dataclass, field, fields, is_dataclass, MISSING
4
+ from enum import Enum, Flag
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+
10
+ class AutoDataclassMeta(type):
11
+ def __new__(cls, name, bases, dct):
12
+ new_cls = super().__new__(cls, name, bases, dct)
13
+ return dataclass(init=False)(new_cls)
14
+
15
+
16
+ @dataclass
17
+ class PartomaticConfig(metaclass=AutoDataclassMeta):
18
+ yaml_tree: str = "Part"
19
+ stl_folder: str = "NONE"
20
+ file_prefix: str = ""
21
+ file_suffix: str = ""
22
+ create_folders_if_missing: bool = True
23
+
24
+ def _default_config(self):
25
+ """
26
+ Resets all values to their default values.
27
+ """
28
+ for field in fields(self):
29
+ if field.default is not MISSING:
30
+ setattr(self, field.name, field.default)
31
+ elif field.default_factory is not MISSING:
32
+ setattr(self, field.name, field.default_factory())
33
+ else:
34
+ raise ValueError(f"Field {field.name} has no default value")
35
+
36
+ def load_config(self, configuration: any, **kwargs):
37
+ """
38
+ loads a partomatic configuration from a file or valid yaml
39
+ -------
40
+ arguments:
41
+ - configuration: the path to the configuration file
42
+ OR
43
+ a valid yaml configuration string
44
+ -------
45
+ notes:
46
+ if yaml_tree is set in the PartomaticConfig descendent,
47
+ PartomaticConfig will use that tree to find a node deep
48
+ within the yaml tree, following the node names separated by slashes
49
+ (example: "BigObject/Partomatic")
50
+ """
51
+ if "yaml_tree" in kwargs:
52
+ self.yaml_tree = kwargs["yaml_tree"]
53
+ if isinstance(configuration, self.__class__):
54
+ for field in fields(self):
55
+ setattr(self, field.name, getattr(configuration, field.name))
56
+ return
57
+ if configuration is not None:
58
+ configuration = str(configuration)
59
+ if "\n" not in configuration:
60
+ path = Path(configuration)
61
+ if path.exists() and path.is_file():
62
+ configuration = path.read_text()
63
+ bracket_dict = yaml.safe_load(configuration)
64
+ for node in self.yaml_tree.split("/"):
65
+ if node not in bracket_dict:
66
+ raise ValueError(
67
+ f"Node {node} not found in configuration file"
68
+ )
69
+ bracket_dict = bracket_dict[node]
70
+
71
+ for classfield in fields(self.__class__):
72
+ if classfield.name in bracket_dict:
73
+ value = bracket_dict[classfield.name]
74
+ if isinstance(classfield.type, type) and issubclass(
75
+ classfield.type, (Enum, Flag)
76
+ ):
77
+ setattr(
78
+ self,
79
+ classfield.name,
80
+ classfield.type[value.upper()],
81
+ )
82
+ elif is_dataclass(classfield.type) and isinstance(
83
+ value, dict
84
+ ):
85
+ setattr(
86
+ self,
87
+ classfield.name,
88
+ classfield.type(**value),
89
+ )
90
+ else:
91
+ setattr(self, classfield.name, value)
92
+
93
+ def __init__(self, configuration: any = None, **kwargs):
94
+ """
95
+ loads a partomatic configuration from a file or valid yaml
96
+ -------
97
+ arguments:
98
+ - configuration: the path to the configuration file
99
+ OR
100
+ a valid yaml configuration string
101
+ OR
102
+ None (default) for an empty object
103
+ - **kwargs: specific fields to set in the configuration
104
+ -------
105
+ notes:
106
+ you can assign yaml_tree as a kwarg here to load a
107
+ configuration from a node node deep within the yaml tree,
108
+ following the node names separated by slashes
109
+ (example: "BigObject/Partomatic")
110
+ """
111
+ if "yaml_tree" in kwargs:
112
+ self.yaml_tree = kwargs["yaml_tree"]
113
+ if configuration is not None:
114
+ self.load_config(configuration, yaml_tree=self.yaml_tree)
115
+ elif kwargs:
116
+ self._default_config()
117
+ for key, value in kwargs.items():
118
+ classfield = next(
119
+ (f for f in fields(self.__class__) if f.name == key),
120
+ None,
121
+ )
122
+ if classfield:
123
+ if is_dataclass(classfield.type):
124
+ if isinstance(value, dict):
125
+ setattr(self, key, classfield.type(**value))
126
+ else:
127
+ setattr(self, key, value)
128
+ else:
129
+ setattr(self, key, value)
130
+ else:
131
+ self._default_config()
@@ -0,0 +1,6 @@
1
+ import sys
2
+ import os
3
+
4
+ sys.path.insert(
5
+ 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))
6
+ )
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum, auto
3
+ import pytest
4
+ from unittest.mock import patch
5
+ from pathlib import Path
6
+
7
+ from partomatic import BuildablePart
8
+ from build123d import BuildPart, Box, Sphere, Align, Mode, Location, Part
9
+
10
+
11
+ class TestBuildablePart:
12
+ def test_extension_removed(self):
13
+ widget_part = Part()
14
+ widget = BuildablePart(widget_part, "widget.stl")
15
+ assert widget.file_name == "widget"
@@ -0,0 +1,178 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum, auto
3
+ import pytest
4
+ from unittest.mock import patch
5
+ from pathlib import Path
6
+
7
+ from partomatic import BuildablePart, PartomaticConfig, Partomatic
8
+ from build123d import BuildPart, Box, Part, Sphere, Align, Mode, Location
9
+
10
+
11
+ class FakeEnum(Enum):
12
+ ONE = auto()
13
+ TWO = auto()
14
+ THREE = auto()
15
+
16
+
17
+ class SubConfig(PartomaticConfig):
18
+ sub_field: str = "sub_default"
19
+ sub_enum: FakeEnum = FakeEnum.ONE
20
+
21
+
22
+ class ContainerConfig(PartomaticConfig):
23
+ container_field: str = "container_default"
24
+ sub: SubConfig = field(default_factory=SubConfig)
25
+
26
+
27
+ class TestPartomaticConfig:
28
+ config_yaml = """
29
+ Part:
30
+ stl_folder: "yaml_folder"
31
+ file_prefix: "yaml_prefix"
32
+ file_suffix: "yaml_suffix"
33
+ """
34
+ blah_config_yaml = """
35
+ Foo:
36
+ container_field: "yaml_container_field"
37
+ Blah:
38
+ stl_folder: "yaml_blah_folder"
39
+ file_prefix: "yaml_blah_prefix"
40
+ file_suffix: "yaml_blah_suffix"
41
+ sub_field: "yaml_sub_field"
42
+ sub_enum: "TWO"
43
+ """
44
+
45
+ sub_config_yaml = """
46
+ Part:
47
+ container_field: "yaml_container_field"
48
+ sub:
49
+ stl_folder: "yaml_blah_folder"
50
+ file_prefix: "yaml_blah_prefix"
51
+ file_suffix: "yaml_blah_suffix"
52
+ sub_field: "yaml_sub_field"
53
+ sub_enum: "TWO"
54
+ """
55
+
56
+ def test_yaml_partomat(self):
57
+ config = PartomaticConfig(self.config_yaml)
58
+ assert config.stl_folder == "yaml_folder"
59
+
60
+ def test_empty_partomat(self):
61
+ config = PartomaticConfig()
62
+ assert config.stl_folder == "NONE"
63
+
64
+ def test_subconfig(self):
65
+ config = SubConfig(self.blah_config_yaml, yaml_tree="Foo/Blah")
66
+ assert config.stl_folder == "yaml_blah_folder"
67
+ assert config.sub_field == "yaml_sub_field"
68
+ assert config.sub_enum == FakeEnum.TWO
69
+
70
+ def test_kwargs(self):
71
+ config = SubConfig(yaml_tree="Part/Blah", sub_field="kwargsub")
72
+ assert config.stl_folder == "NONE"
73
+ assert config.sub_field == "kwargsub"
74
+
75
+ def test_yaml_container_partomat(self):
76
+ config = ContainerConfig(self.sub_config_yaml)
77
+ assert config.container_field == "yaml_container_field"
78
+ assert config.sub.sub_field == "yaml_sub_field"
79
+
80
+ def test_invalid_config(self):
81
+ with pytest.raises(ValueError):
82
+ ContainerConfig("invalid_config")
83
+
84
+ def test_yaml_container_with_dict_partomat(self):
85
+ config = ContainerConfig(
86
+ sub={
87
+ "stl_folder": "yaml_blah_folder",
88
+ "file_prefix": "yaml_blah_prefix",
89
+ "file_suffix": "yaml_blah_suffix",
90
+ "sub_field": "yaml_sub_field",
91
+ "sub_enum": "TWO",
92
+ }
93
+ )
94
+ assert config.sub.sub_field == "yaml_sub_field"
95
+
96
+ def test_yaml_container_with_class_partomat(self):
97
+ sub_config = SubConfig(self.blah_config_yaml, yaml_tree="Foo/Blah")
98
+
99
+ config = ContainerConfig(sub=sub_config)
100
+ assert config.sub.sub_field == "yaml_sub_field"
101
+
102
+ def test_default_container_partomat(self):
103
+ config = ContainerConfig()
104
+ assert config.container_field == "container_default"
105
+ assert config.sub.sub_field == "sub_default"
106
+
107
+ def test_config_create(self):
108
+ config = PartomaticConfig()
109
+ config.stl_folder = "config_create_folder"
110
+ config = PartomaticConfig(config)
111
+ assert config.stl_folder == "config_create_folder"
112
+
113
+
114
+ class TestBuildablePart:
115
+ def test_extension_removed(self):
116
+ widget_part = Part()
117
+ widget = BuildablePart(widget_part, "widget.stl")
118
+ assert widget.file_name == "widget"
119
+
120
+
121
+ @dataclass
122
+ class WidgetConfig(PartomaticConfig):
123
+ stl_folder: str = field(default="C:\\Users\\xopher\\Downloads")
124
+ radius: float = field(default=10)
125
+ length: float = field(default=17)
126
+
127
+
128
+ class Widget(Partomatic):
129
+
130
+ _config: WidgetConfig = WidgetConfig()
131
+
132
+ def compile(self):
133
+ self.parts.clear()
134
+ with BuildPart() as holebox:
135
+ Box(
136
+ self._config.length,
137
+ self._config.length,
138
+ self._config.length,
139
+ align=(Align.CENTER, Align.CENTER, Align.CENTER),
140
+ )
141
+ Sphere(
142
+ self._config.radius,
143
+ align=(Align.CENTER, Align.CENTER, Align.CENTER),
144
+ mode=Mode.SUBTRACT,
145
+ )
146
+
147
+ self.parts.append(
148
+ BuildablePart(
149
+ holebox.part,
150
+ "test",
151
+ display_location=Location((9, 0, 9)),
152
+ stl_folder=str(Path(self._config.stl_folder) / "stls"),
153
+ step_folder=str(Path(self._config.stl_folder) / "steps"),
154
+ create_folders=True,
155
+ )
156
+ )
157
+
158
+
159
+ class TestPartomatic:
160
+
161
+ def test_partomatic_class(self):
162
+ wc = WidgetConfig()
163
+ assert wc.stl_folder == "C:\\Users\\xopher\\Downloads"
164
+ foo = Widget(wc)
165
+ assert foo._config.radius == 10
166
+ assert foo._config.length == 17
167
+ with (
168
+ patch("pathlib.Path.mkdir"),
169
+ patch("pathlib.Path.exists"),
170
+ patch("pathlib.Path.is_dir"),
171
+ patch("ocp_vscode.show"),
172
+ patch("build123d.export_stl"),
173
+ patch("build123d.export_step"),
174
+ ):
175
+ foo.display()
176
+ foo.partomate()
177
+ foo._config.stl_folder = "NONE"
178
+ foo.export_stls()
@@ -0,0 +1,112 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum, auto
3
+ import pytest
4
+ from unittest.mock import patch
5
+ from pathlib import Path
6
+ from partomatic import PartomaticConfig
7
+
8
+
9
+ from build123d import BuildPart, Box, Sphere, Align, Mode, Location, Part
10
+
11
+
12
+ class FakeEnum(Enum):
13
+ ONE = auto()
14
+ TWO = auto()
15
+ THREE = auto()
16
+
17
+
18
+ class SubConfig(PartomaticConfig):
19
+ sub_field: str = "sub_default"
20
+ sub_enum: FakeEnum = FakeEnum.ONE
21
+
22
+
23
+ class ContainerConfig(PartomaticConfig):
24
+ container_field: str = "container_default"
25
+ sub: SubConfig = field(default_factory=SubConfig)
26
+
27
+
28
+ class TestPartomaticConfig:
29
+ config_yaml = """
30
+ Part:
31
+ stl_folder: "yaml_folder"
32
+ file_prefix: "yaml_prefix"
33
+ file_suffix: "yaml_suffix"
34
+ """
35
+ blah_config_yaml = """
36
+ Foo:
37
+ container_field: "yaml_container_field"
38
+ Blah:
39
+ stl_folder: "yaml_blah_folder"
40
+ file_prefix: "yaml_blah_prefix"
41
+ file_suffix: "yaml_blah_suffix"
42
+ sub_field: "yaml_sub_field"
43
+ sub_enum: "TWO"
44
+ """
45
+
46
+ sub_config_yaml = """
47
+ Part:
48
+ container_field: "yaml_container_field"
49
+ sub:
50
+ stl_folder: "yaml_blah_folder"
51
+ file_prefix: "yaml_blah_prefix"
52
+ file_suffix: "yaml_blah_suffix"
53
+ sub_field: "yaml_sub_field"
54
+ sub_enum: "TWO"
55
+ """
56
+
57
+ def test_yaml_partomat(self):
58
+ config = PartomaticConfig(self.config_yaml)
59
+ assert config.stl_folder == "yaml_folder"
60
+
61
+ def test_empty_partomat(self):
62
+ config = PartomaticConfig()
63
+ assert config.stl_folder == "NONE"
64
+
65
+ def test_subconfig(self):
66
+ config = SubConfig(self.blah_config_yaml, yaml_tree="Foo/Blah")
67
+ assert config.stl_folder == "yaml_blah_folder"
68
+ assert config.sub_field == "yaml_sub_field"
69
+ assert config.sub_enum == FakeEnum.TWO
70
+
71
+ def test_kwargs(self):
72
+ config = SubConfig(yaml_tree="Part/Blah", sub_field="kwargsub")
73
+ assert config.stl_folder == "NONE"
74
+ assert config.sub_field == "kwargsub"
75
+
76
+ def test_yaml_container_partomat(self):
77
+ config = ContainerConfig(self.sub_config_yaml)
78
+ assert config.container_field == "yaml_container_field"
79
+ assert config.sub.sub_field == "yaml_sub_field"
80
+
81
+ def test_invalid_config(self):
82
+ with pytest.raises(ValueError):
83
+ ContainerConfig("invalid_config")
84
+
85
+ def test_yaml_container_with_dict_partomat(self):
86
+ config = ContainerConfig(
87
+ sub={
88
+ "stl_folder": "yaml_blah_folder",
89
+ "file_prefix": "yaml_blah_prefix",
90
+ "file_suffix": "yaml_blah_suffix",
91
+ "sub_field": "yaml_sub_field",
92
+ "sub_enum": "TWO",
93
+ }
94
+ )
95
+ assert config.sub.sub_field == "yaml_sub_field"
96
+
97
+ def test_yaml_container_with_class_partomat(self):
98
+ sub_config = SubConfig(self.blah_config_yaml, yaml_tree="Foo/Blah")
99
+
100
+ config = ContainerConfig(sub=sub_config)
101
+ assert config.sub.sub_field == "yaml_sub_field"
102
+
103
+ def test_default_container_partomat(self):
104
+ config = ContainerConfig()
105
+ assert config.container_field == "container_default"
106
+ assert config.sub.sub_field == "sub_default"
107
+
108
+ def test_config_create(self):
109
+ config = PartomaticConfig()
110
+ config.stl_folder = "config_create_folder"
111
+ config = PartomaticConfig(config)
112
+ assert config.stl_folder == "config_create_folder"