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.
- partomatic-0.0.1/.coverage +0 -0
- partomatic-0.0.1/.gitignore +163 -0
- partomatic-0.0.1/.vscode/settings.json +7 -0
- partomatic-0.0.1/LICENSE +7 -0
- partomatic-0.0.1/PKG-INFO +32 -0
- partomatic-0.0.1/README.md +16 -0
- partomatic-0.0.1/docs/Makefile +20 -0
- partomatic-0.0.1/docs/conf.py +124 -0
- partomatic-0.0.1/docs/index.md +1 -0
- partomatic-0.0.1/docs/partomatic.md +225 -0
- partomatic-0.0.1/docs/requirements.txt +3 -0
- partomatic-0.0.1/mkdocs.yml +20 -0
- partomatic-0.0.1/pyproject.toml +24 -0
- partomatic-0.0.1/readthedocs.yaml +13 -0
- partomatic-0.0.1/requirements.txt +4 -0
- partomatic-0.0.1/src/partomatic/__init__.py +3 -0
- partomatic-0.0.1/src/partomatic/buildable_part.py +39 -0
- partomatic-0.0.1/src/partomatic/partomatic.py +124 -0
- partomatic-0.0.1/src/partomatic/partomatic_config.py +131 -0
- partomatic-0.0.1/tests/conftest.py +6 -0
- partomatic-0.0.1/tests/test_buildablepart.py +15 -0
- partomatic-0.0.1/tests/test_partomatic.py +178 -0
- partomatic-0.0.1/tests/test_partomaticconfig.py +112 -0
|
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/
|
partomatic-0.0.1/LICENSE
ADDED
|
@@ -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,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,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,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"
|