Lots of documentation for slicing related things and some refactorings
This commit is contained in:
parent
b17730137a
commit
f2eeb50381
10 changed files with 655 additions and 140 deletions
|
|
@ -11,4 +11,5 @@ Internal Modules
|
|||
plugin.rst
|
||||
printer.rst
|
||||
settings.rst
|
||||
slicing.rst
|
||||
util.rst
|
||||
|
|
|
|||
13
docs/modules/slicing.rst
Normal file
13
docs/modules/slicing.rst
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.. _sec-modules-slicing:
|
||||
|
||||
octoprint.slicing
|
||||
-----------------
|
||||
|
||||
.. automodule:: octoprint.slicing
|
||||
|
||||
.. _sec-modules-slicing-exceptions:
|
||||
|
||||
octoprint.slicing.exceptions
|
||||
----------------------------
|
||||
|
||||
.. automodule:: octoprint.slicing.exceptions
|
||||
|
|
@ -232,18 +232,17 @@ class FileManager(object):
|
|||
self._slicing_jobs[dest_job_key] = self._slicing_jobs[source_job_key] = (slicer_name, absolute_source_path, temp_path)
|
||||
|
||||
args = (source_location, source_path, temp_path, dest_location, dest_path, start_time, printer_profile_id, callback, callback_args)
|
||||
return self._slicing_manager.slice(
|
||||
slicer_name,
|
||||
absolute_source_path,
|
||||
temp_path,
|
||||
profile,
|
||||
stlProcessed,
|
||||
position=position,
|
||||
callback_args=args,
|
||||
overrides=overrides,
|
||||
printer_profile_id=printer_profile_id,
|
||||
on_progress=self.on_slicing_progress,
|
||||
on_progress_args=(slicer_name, source_location, source_path, dest_location, dest_path))
|
||||
self._slicing_manager.slice(slicer_name,
|
||||
absolute_source_path,
|
||||
temp_path,
|
||||
profile,
|
||||
stlProcessed,
|
||||
position=position,
|
||||
callback_args=args,
|
||||
overrides=overrides,
|
||||
printer_profile_id=printer_profile_id,
|
||||
on_progress=self.on_slicing_progress,
|
||||
on_progress_args=(slicer_name, source_location, source_path, dest_location, dest_path))
|
||||
|
||||
def on_slicing_progress(self, slicer, source_location, source_path, dest_location, dest_path, _progress=None):
|
||||
if not _progress:
|
||||
|
|
|
|||
|
|
@ -18,11 +18,40 @@ import octoprint.util.gcodeInterpreter as gcodeInterpreter
|
|||
|
||||
|
||||
class QueueEntry(collections.namedtuple("QueueEntry", "path, type, location, absolute_path, printer_profile")):
|
||||
"""
|
||||
A :class:`QueueEntry` for processing through the :class:`AnalysisQueue`. Wraps the entry's properties necessary
|
||||
for processing.
|
||||
|
||||
Arguments:
|
||||
path (str): Storage location specific path to the file to analyze.
|
||||
type (str): Type of file to analyze, necessary to map to the correct :class:`AbstractAnalysisQueue` sub class.
|
||||
At the moment, only ``gcode`` is supported here.
|
||||
location (str): Location the file is located on.
|
||||
absolute_path (str): Absolute path on disk through which to access the file.
|
||||
printer_profile (PrinterProfile): :class:`PrinterProfile` which to use for analysis.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return "{location}:{path}".format(location=self.location, path=self.path)
|
||||
|
||||
|
||||
class AnalysisQueue(object):
|
||||
"""
|
||||
OctoPrint's :class:`AnalysisQueue` can manage various :class:`AbstractAnalysisQueue` implementations, mapped
|
||||
by their machine code type.
|
||||
|
||||
At the moment, only the analysis of GCODE files for 3D printing is supported, through :class:`GcodeAnalysisQueue`.
|
||||
|
||||
By invoking :meth:`register_finish_callback` it is possible to register oneself as a callback to be invoked each
|
||||
time the analysis of a queue entry finishes. The call parameters will be the finished queue entry as the first
|
||||
and the analysis result as the second parameter. It is also possible to remove the registration again by invoking
|
||||
:meth:`unregister_finish_callback`.
|
||||
|
||||
:meth:`enqueue` allows enqueuing :class:`QueueEntry` instances to analyze. If the :attr:`QueueEntry.type` is unknown
|
||||
(no specific child class of :class:`AbstractAnalysisQueue` is registered for it), nothing will happen. Otherwise the
|
||||
entry will be enqueued with the type specific analysis queue.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._callbacks = []
|
||||
|
|
@ -56,6 +85,21 @@ class AnalysisQueue(object):
|
|||
eventManager().fire(Events.METADATA_ANALYSIS_FINISHED, {"file": entry.path, "result": result})
|
||||
|
||||
class AbstractAnalysisQueue(object):
|
||||
"""
|
||||
The :class:`AbstractAnalysisQueue` is the parent class of all specific analysis queues such as the
|
||||
:class:`GcodeAnalysisQueue`. It offers methods to enqueue new entries to analyze and pausing and resuming analysis
|
||||
processing.
|
||||
|
||||
Arguments:
|
||||
finished_callback (callable): Callback that will be called upon finishing analysis of an entry in the queue.
|
||||
The callback will be called with the analyzed entry as the first argument and the analysis result as
|
||||
returned from the queue implementation as the second parameter.
|
||||
|
||||
.. automethod:: _do_analysis
|
||||
|
||||
.. automethod:: _do_abort
|
||||
"""
|
||||
|
||||
def __init__(self, finished_callback):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -75,6 +119,18 @@ class AbstractAnalysisQueue(object):
|
|||
self._worker.start()
|
||||
|
||||
def enqueue(self, entry, high_priority=False):
|
||||
"""
|
||||
Enqueues an ``entry`` for analysis by the queue.
|
||||
|
||||
If ``high_priority`` is True (defaults to False), the entry will be prioritized and hence processed before
|
||||
other entries in the queue with normal priority.
|
||||
|
||||
Arguments:
|
||||
entry (QueueEntry): The :class:`QueueEntry` to analyze.
|
||||
high_priority (boolean): Whether to process the provided entry with high priority (True) or not
|
||||
(False, default)
|
||||
"""
|
||||
|
||||
if high_priority:
|
||||
self._logger.debug("Adding entry {entry} to analysis queue with high priority".format(entry=entry))
|
||||
prio = 0
|
||||
|
|
@ -85,6 +141,10 @@ class AbstractAnalysisQueue(object):
|
|||
self._queue.put((prio, entry))
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pauses processing of the queue, e.g. when a print is active.
|
||||
"""
|
||||
|
||||
self._logger.debug("Pausing analysis")
|
||||
self._active.clear()
|
||||
if self._current is not None:
|
||||
|
|
@ -92,6 +152,10 @@ class AbstractAnalysisQueue(object):
|
|||
self._do_abort()
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resumes processing of the queue, e.g. when a print has finished.
|
||||
"""
|
||||
|
||||
self._logger.debug("Resuming analyzer")
|
||||
self._active.set()
|
||||
|
||||
|
|
@ -134,13 +198,42 @@ class AbstractAnalysisQueue(object):
|
|||
self._current_progress = None
|
||||
|
||||
def _do_analysis(self):
|
||||
"""
|
||||
Performs the actual analysis of the current entry which can be accessed via ``self._current``. Needs to be
|
||||
overridden by sub classes.
|
||||
|
||||
Returns:
|
||||
object: The result of the analysis which will be forwarded to the ``finished_callback`` provided during
|
||||
construction.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _do_abort(self):
|
||||
"""
|
||||
Aborts analysis of the current entry. Needs to be overridden by sub classes.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class GcodeAnalysisQueue(AbstractAnalysisQueue):
|
||||
"""
|
||||
A queue to analyze GCODE files. Analysis results are :class:`dict` instances structured as follows:
|
||||
|
||||
.. list-table::
|
||||
:widths: 25 70
|
||||
|
||||
- * **Key**
|
||||
* **Description**
|
||||
- * ``estimatedPrintTime``
|
||||
* Estimated time the file take to print, in minutes
|
||||
- * ``filament``
|
||||
* Substructure describing estimated filament usage. Keys are ``tool0`` for the first extruder, ``tool1`` for
|
||||
the second and so on. For each tool extruded length and volume (based on diameter) are provided.
|
||||
- * ``filament.toolX.length``
|
||||
* The extruded length in mm
|
||||
- * ``filament.toolX.volume``
|
||||
* The extruded volume in cm³
|
||||
"""
|
||||
|
||||
def _do_analysis(self):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -869,9 +869,10 @@ class EventHandlerPlugin(OctoPrintPlugin):
|
|||
"""
|
||||
Called by OctoPrint upon processing of a fired event on the platform.
|
||||
|
||||
:param string event: the type of event that got fired, see :ref:`the list of events <sec-events-available_events>`
|
||||
for possible values
|
||||
:param dict payload: the payload as provided with the event
|
||||
Arguments:
|
||||
event (str): The type of event that got fired, see :ref:`the list of events <sec-events-available_events>`
|
||||
for possible values
|
||||
payload (dict): The payload as provided with the event
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -903,10 +904,14 @@ class SlicerPlugin(OctoPrintPlugin):
|
|||
name
|
||||
The human readable name of the slicer. This will be displayed to the user during slicer selection.
|
||||
same_device
|
||||
``True`` if the slicer runs on the same device as OctoPrint, ``False`` otherwise. Slicers running on the same
|
||||
device will TODO
|
||||
True if the slicer runs on the same device as OctoPrint, False otherwise. Slicers running on the same
|
||||
device will not be allowed to slice while a print is running due to performance reasons. Slice requests
|
||||
against slicers running on the same device will result in an error.
|
||||
progress_report
|
||||
``True`` if the slicer can report back slicing progress to OctoPrint ``False`` otherwise.
|
||||
|
||||
Returns:
|
||||
dict: A dict describing the slicer as outlined above.
|
||||
"""
|
||||
return dict(
|
||||
type=None,
|
||||
|
|
@ -917,38 +922,48 @@ class SlicerPlugin(OctoPrintPlugin):
|
|||
|
||||
def get_slicer_default_profile(self):
|
||||
"""
|
||||
Should return a :class:`SlicingProfile` containing the default slicing profile to use with this slicer if
|
||||
no other profile has been selected.
|
||||
Should return a :class:`~octoprint.slicing.SlicingProfile` containing the default slicing profile to use with
|
||||
this slicer if no other profile has been selected.
|
||||
|
||||
Returns:
|
||||
SlicingProfile: The :class:`~octoprint.slicing.SlicingProfile` containing the default slicing profile for
|
||||
this slicer.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_slicer_profile(self, path):
|
||||
"""
|
||||
Should return a :class:`SlicingProfile` parsed from the slicing profile stored at the indicated ``path``.
|
||||
Should return a :class:`~octoprint.slicing.SlicingProfile` parsed from the slicing profile stored at the
|
||||
indicated ``path``.
|
||||
|
||||
:param string path: the path from which to read the slicing profile
|
||||
Arguments:
|
||||
path (str): The absolute path from which to read the slicing profile.
|
||||
|
||||
Returns:
|
||||
SlicingProfile: The specified slicing profile.
|
||||
"""
|
||||
return None
|
||||
|
||||
def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=None):
|
||||
"""
|
||||
Should save the provided :class:`SlicingProfile` to the indicated ``path``, after applying any supplied
|
||||
``overrides``. If a profile is already saved under the indicated path and ``allow_overwrite`` is set to
|
||||
``False`` (defaults to ``True``), an ``IOError`` should be raised.
|
||||
Should save the provided :class:`~octoprint.slicing.SlicingProfile` to the indicated ``path``, after applying
|
||||
any supplied ``overrides``. If a profile is already saved under the indicated path and ``allow_overwrite`` is
|
||||
set to False (defaults to True), an :class:`IOError` should be raised.
|
||||
|
||||
:param string path: the path to which to save the profile
|
||||
:param SlicingProfile profile: the profile to save
|
||||
:param bool allow_overwrite: whether to allow to overwrite an existing profile at the indicated path (``True``, default)
|
||||
or not (``False``) - if a profile already exists on the path and this is ``False``
|
||||
and :class:`IOError` should be raised
|
||||
:param dict overrides: profile overrides to apply to the ``profile`` before saving it
|
||||
Arguments:
|
||||
path (str): The absolute path to which to save the profile.
|
||||
profile (SlicingProfile): The profile to save.
|
||||
allow_overwrite (boolean): Whether to allow to overwrite an existing profile at the indicated path (True,
|
||||
default) or not (False). If a profile already exists on teh path and this is False an
|
||||
:class:`IOError` should be raised.
|
||||
overrides (dict): Profile overrides to apply to the ``profile`` before saving it
|
||||
"""
|
||||
pass
|
||||
|
||||
def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_path=None, position=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
"""
|
||||
Called by OctoPrint to slice ``model_path`` for the indicated ``printer_profile``. If the ``machinecode_path`` is ``None``,
|
||||
slicer implementations should generate it from the provided model_path.
|
||||
slicer implementations should generate it from the provided ``model_path``.
|
||||
|
||||
If provided, the ``profile_path`` is guaranteed by OctoPrint to be a serialized slicing profile created through the slicing
|
||||
plugin's own :func:`save_slicer_profile` method.
|
||||
|
|
@ -973,12 +988,40 @@ class SlicerPlugin(OctoPrintPlugin):
|
|||
|
||||
Please note that both ``on_progress_args`` and ``on_progress_kwargs`` as supplied by OctoPrint might be ``None``,
|
||||
so always make sure to initialize those values to sane defaults like depicted above before invoking the callback.
|
||||
|
||||
In order to support external cancellation of an ongoing slicing job via :func:`cancel_slicing`, implementations
|
||||
should make sure to track the started jobs via the ``machinecode_path``, if provided.
|
||||
|
||||
The method should return a 2-tuple consisting of a boolean ``flag`` indicating whether the slicing job was
|
||||
finished successfully (True) or not (False) and a ``result`` depending on the success of the slicing job.
|
||||
|
||||
For jobs that finished successfully, ``result`` should be a :class:`dict` containing additional information
|
||||
about the slicing job under the following keys:
|
||||
|
||||
_analysis
|
||||
Analysis result of the generated machine code as returned by the slicer itself. This should match the
|
||||
data structure described for the analysis queue of the matching maching code format, e.g.
|
||||
:class:`~octoprint.filemanager.analysis.GcodeAnalysisQueue` for GCODE files.
|
||||
|
||||
For jobs that did not finish successfully (but not due to being cancelled!), ``result`` should be a :class:`str`
|
||||
containing a human readable reason for the error.
|
||||
|
||||
If the job gets cancelled, a :class:`~octoprint.slicing.SlicingCancelled` exception should be raised.
|
||||
|
||||
Returns:
|
||||
tuple: A 2-tuple (boolean, object) as outlined above.
|
||||
|
||||
Raises:
|
||||
SlicingCancelled: The slicing job was cancelled (via :meth:`cancel_slicing`).
|
||||
"""
|
||||
pass
|
||||
|
||||
def cancel_slicing(self, machinecode_path):
|
||||
"""
|
||||
Cancels the slicing to the indicated file
|
||||
Cancels the slicing to the indicated file.
|
||||
|
||||
Arguments:
|
||||
machinecode_path (str): The absolute path to the machine code file to which to stop slicing to.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -106,12 +106,15 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
from octoprint.server.api import valid_boolean_trues
|
||||
profile_allow_overwrite = flask.request.values["allowOverwrite"] in valid_boolean_trues
|
||||
|
||||
slicingManager.save_profile("cura",
|
||||
profile_name,
|
||||
profile_dict,
|
||||
allow_overwrite=profile_allow_overwrite,
|
||||
display_name=profile_display_name,
|
||||
description=profile_description)
|
||||
try:
|
||||
slicingManager.save_profile("cura",
|
||||
profile_name,
|
||||
profile_dict,
|
||||
allow_overwrite=profile_allow_overwrite,
|
||||
display_name=profile_display_name,
|
||||
description=profile_description)
|
||||
except octoprint.slicing.ProfileAlreadyExists:
|
||||
return flask.make_response("A profile named {profile_name} already exists for slicer cura".format(**locals()), 409)
|
||||
|
||||
result = dict(
|
||||
resource=flask.url_for("api.slicingGetSlicerProfile", slicer="cura", name=profile_name, _external=True),
|
||||
|
|
@ -191,6 +194,9 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
return octoprint.slicing.SlicingProfile(properties["type"], "unknown", profile_dict, display_name=display_name, description=description)
|
||||
|
||||
def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=None):
|
||||
if os.path.exists(path) and not allow_overwrite:
|
||||
raise octoprint.slicing.ProfileAlreadyExists("cura", profile.name)
|
||||
|
||||
new_profile = Profile.merge_profile(profile.data, overrides=overrides)
|
||||
|
||||
if profile.display_name is not None:
|
||||
|
|
@ -388,9 +394,6 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
return profile_dict
|
||||
|
||||
def _save_profile(self, path, profile, allow_overwrite=True):
|
||||
if not allow_overwrite and os.path.exists(path):
|
||||
raise IOError("Cannot overwrite {path}".format(path=path))
|
||||
|
||||
import yaml
|
||||
with open(path, "wb") as f:
|
||||
yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from octoprint.server.util.flask import restricted_access, get_json_command_from
|
|||
from octoprint.server.api import api
|
||||
from octoprint.events import Events
|
||||
import octoprint.filemanager
|
||||
import octoprint.slicing
|
||||
|
||||
|
||||
#~~ GCODE file handling
|
||||
|
|
@ -297,17 +298,20 @@ def gcodeFileCommand(filename, target):
|
|||
printer.select_file(filenameToSelect, sd, printAfterLoading)
|
||||
|
||||
elif command == "slice":
|
||||
if "slicer" in data.keys():
|
||||
slicer = data["slicer"]
|
||||
del data["slicer"]
|
||||
if not slicer in slicingManager.registered_slicers:
|
||||
return make_response("Slicer {slicer} is not available".format(**locals()), 400)
|
||||
slicer_instance = slicingManager.get_slicer(slicer)
|
||||
elif "cura" in slicingManager.registered_slicers:
|
||||
slicer = "cura"
|
||||
slicer_instance = slicingManager.get_slicer("cura")
|
||||
else:
|
||||
return make_response("Cannot slice {filename}, no slicer available".format(**locals()), 415)
|
||||
try:
|
||||
if "slicer" in data:
|
||||
slicer = data["slicer"]
|
||||
del data["slicer"]
|
||||
slicer_instance = slicingManager.get_slicer(slicer)
|
||||
|
||||
elif "cura" in slicingManager.registered_slicers:
|
||||
slicer = "cura"
|
||||
slicer_instance = slicingManager.get_slicer("cura")
|
||||
|
||||
else:
|
||||
return make_response("Cannot slice {filename}, no slicer available".format(**locals()), 415)
|
||||
except octoprint.slicing.UnknownSlicer as e:
|
||||
return make_response("Slicer {slicer} is not available".format(slicer=e.slicer), 400)
|
||||
|
||||
if not octoprint.filemanager.valid_file_type(filename, type="stl"):
|
||||
return make_response("Cannot slice {filename}, not an STL file".format(**locals()), 415)
|
||||
|
|
@ -316,7 +320,7 @@ def gcodeFileCommand(filename, target):
|
|||
# slicer runs on same device as OctoPrint, slicing while printing is hence disabled
|
||||
return make_response("Cannot slice on {slicer} while printing due to performance reasons".format(**locals()), 409)
|
||||
|
||||
if "gcode" in data.keys() and data["gcode"]:
|
||||
if "gcode" in data and data["gcode"]:
|
||||
gcode_name = data["gcode"]
|
||||
del data["gcode"]
|
||||
else:
|
||||
|
|
@ -374,31 +378,31 @@ def gcodeFileCommand(filename, target):
|
|||
filenameToSelect = fileManager.path_on_disk(target, gcode_name)
|
||||
printer.select_file(filenameToSelect, sd, print_after_slicing)
|
||||
|
||||
ok, result = fileManager.slice(slicer, target, filename, target, gcode_name,
|
||||
profile=profile,
|
||||
printer_profile_id=printerProfile,
|
||||
position=position,
|
||||
overrides=overrides,
|
||||
callback=slicing_done,
|
||||
callback_args=(target, gcode_name, select_after_slicing, print_after_slicing))
|
||||
try:
|
||||
fileManager.slice(slicer, target, filename, target, gcode_name,
|
||||
profile=profile,
|
||||
printer_profile_id=printerProfile,
|
||||
position=position,
|
||||
overrides=overrides,
|
||||
callback=slicing_done,
|
||||
callback_args=(target, gcode_name, select_after_slicing, print_after_slicing))
|
||||
except octoprint.slicing.UnknownProfile:
|
||||
return make_response("Profile {profile} doesn't exist".format(**locals()), 400)
|
||||
|
||||
if ok:
|
||||
files = {}
|
||||
location = url_for(".readGcodeFile", target=target, filename=gcode_name, _external=True)
|
||||
result = {
|
||||
"name": gcode_name,
|
||||
"origin": FileDestinations.LOCAL,
|
||||
"refs": {
|
||||
"resource": location,
|
||||
"download": url_for("index", _external=True) + "downloads/files/" + target + "/" + gcode_name
|
||||
}
|
||||
files = {}
|
||||
location = url_for(".readGcodeFile", target=target, filename=gcode_name, _external=True)
|
||||
result = {
|
||||
"name": gcode_name,
|
||||
"origin": FileDestinations.LOCAL,
|
||||
"refs": {
|
||||
"resource": location,
|
||||
"download": url_for("index", _external=True) + "downloads/files/" + target + "/" + gcode_name
|
||||
}
|
||||
}
|
||||
|
||||
r = make_response(jsonify(result), 202)
|
||||
r.headers["Location"] = location
|
||||
return r
|
||||
else:
|
||||
return make_response("Could not slice: {result}".format(result=result), 500)
|
||||
r = make_response(jsonify(result), 202)
|
||||
r.headers["Location"] = location
|
||||
return r
|
||||
|
||||
return NO_CONTENT
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from octoprint.server.api import api, NO_CONTENT
|
|||
|
||||
from octoprint.settings import settings as s, valid_boolean_trues
|
||||
|
||||
from octoprint.slicing import SlicerNotConfigured
|
||||
from octoprint.slicing import UnknownSlicer, SlicerNotConfigured, ProfileAlreadyExists, UnknownProfile
|
||||
|
||||
|
||||
@api.route("/slicing", methods=["GET"])
|
||||
|
|
@ -28,37 +28,39 @@ def slicingListAll():
|
|||
|
||||
result = dict()
|
||||
for slicer in slicers:
|
||||
slicer_impl = slicingManager.get_slicer(slicer, require_configured=False)
|
||||
result[slicer] = dict(
|
||||
key=slicer,
|
||||
displayName=slicer_impl.get_slicer_properties()["name"],
|
||||
default=default_slicer == slicer,
|
||||
configured = slicer_impl.is_slicer_configured(),
|
||||
profiles=_getSlicingProfilesData(slicer)
|
||||
)
|
||||
try:
|
||||
slicer_impl = slicingManager.get_slicer(slicer, require_configured=False)
|
||||
result[slicer] = dict(
|
||||
key=slicer,
|
||||
displayName=slicer_impl.get_slicer_properties()["name"],
|
||||
default=default_slicer == slicer,
|
||||
configured = slicer_impl.is_slicer_configured(),
|
||||
profiles=_getSlicingProfilesData(slicer)
|
||||
)
|
||||
except (UnknownSlicer, SlicerNotConfigured):
|
||||
# this should never happen
|
||||
pass
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@api.route("/slicing/<string:slicer>/profiles", methods=["GET"])
|
||||
def slicingListSlicerProfiles(slicer):
|
||||
if not slicer in slicingManager.registered_slicers:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
configured = False
|
||||
if "configured" in request.values and request.values["configured"] in valid_boolean_trues:
|
||||
if not slicer in slicingManager.configured_slicers:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
configured = True
|
||||
|
||||
return jsonify(_getSlicingProfilesData(slicer, require_configured=configured))
|
||||
try:
|
||||
return jsonify(_getSlicingProfilesData(slicer, require_configured=configured))
|
||||
except (UnknownSlicer, SlicerNotConfigured):
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["GET"])
|
||||
def slicingGetSlicerProfile(slicer, name):
|
||||
if not slicer in slicingManager.registered_slicers:
|
||||
try:
|
||||
profile = slicingManager.load_profile(slicer, name)
|
||||
except UnknownSlicer:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
profile = slicingManager.load_profile(slicer, name)
|
||||
if not profile:
|
||||
except UnknownProfile:
|
||||
return make_response("Profile not found", 404)
|
||||
|
||||
result = _getSlicingProfileData(slicer, name, profile)
|
||||
|
|
@ -68,9 +70,6 @@ def slicingGetSlicerProfile(slicer, name):
|
|||
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["PUT"])
|
||||
@restricted_access
|
||||
def slicingAddSlicerProfile(slicer, name):
|
||||
if not slicer in slicingManager.registered_slicers:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return make_response("Expected content-type JSON", 400)
|
||||
|
||||
|
|
@ -89,7 +88,12 @@ def slicingAddSlicerProfile(slicer, name):
|
|||
if "description" in json_data:
|
||||
description = json_data["description"]
|
||||
|
||||
profile = slicingManager.save_profile(slicer, name, data, display_name=display_name, description=description)
|
||||
try:
|
||||
profile = slicingManager.save_profile(slicer, name, data, display_name=display_name, description=description)
|
||||
except UnknownSlicer:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
except ProfileAlreadyExists:
|
||||
return make_response("A profile named {name} already exists for slicer {slicer}".format(**locals()), 409)
|
||||
|
||||
result = _getSlicingProfileData(slicer, name, profile)
|
||||
r = make_response(jsonify(result), 201)
|
||||
|
|
@ -99,15 +103,15 @@ def slicingAddSlicerProfile(slicer, name):
|
|||
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["PATCH"])
|
||||
@restricted_access
|
||||
def slicingPatchSlicerProfile(slicer, name):
|
||||
if not slicer in slicingManager.registered_slicers:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return make_response("Expected content-type JSON", 400)
|
||||
|
||||
profile = slicingManager.load_profile(slicer, name)
|
||||
if not profile:
|
||||
return make_response("Profile not found", 404)
|
||||
try:
|
||||
profile = slicingManager.load_profile(slicer, name)
|
||||
except UnknownSlicer:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
except UnknownProfile:
|
||||
return make_response("Profile {name} for slicer {slicer} not found".format(**locals()), 404)
|
||||
|
||||
try:
|
||||
json_data = request.json
|
||||
|
|
@ -133,23 +137,26 @@ def slicingPatchSlicerProfile(slicer, name):
|
|||
s().set(["slicing", "defaultProfiles"], default_profiles)
|
||||
s().save(force=True)
|
||||
|
||||
slicingManager.save_profile(slicer, name, profile, overrides=data, display_name=display_name, description=description)
|
||||
return NO_CONTENT
|
||||
try:
|
||||
saved_profile = slicingManager.save_profile(slicer, name, profile, overrides=data, display_name=display_name, description=description)
|
||||
except ProfileAlreadyExists:
|
||||
return make_response("Profile named {name} for slicer {slicer} does already exist".format(**locals()), 409)
|
||||
return jsonify(_getSlicingProfileData(slicer, name, saved_profile))
|
||||
|
||||
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def slicingDelSlicerProfile(slicer, name):
|
||||
if not slicer in slicingManager.registered_slicers:
|
||||
try:
|
||||
slicingManager.delete_profile(slicer, name)
|
||||
except UnknownSlicer:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
except UnknownProfile:
|
||||
return make_response("Unknown profile {name} for slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
slicingManager.delete_profile(slicer, name)
|
||||
return NO_CONTENT
|
||||
|
||||
def _getSlicingProfilesData(slicer, require_configured=False):
|
||||
try:
|
||||
profiles = slicingManager.all_profiles(slicer, require_configured=require_configured)
|
||||
except SlicerNotConfigured:
|
||||
return dict()
|
||||
profiles = slicingManager.all_profiles(slicer, require_configured=require_configured)
|
||||
|
||||
result = dict()
|
||||
for name, profile in profiles.items():
|
||||
|
|
|
|||
|
|
@ -1,4 +1,17 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
In this module the slicing support of OctoPrint is encapsulated.
|
||||
|
||||
.. autoclass:: SlicingProfile
|
||||
:members:
|
||||
|
||||
.. autoclass:: TemporaryProfile
|
||||
:members:
|
||||
|
||||
.. autoclass:: SlicingManager
|
||||
:members:
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
|
|
@ -15,6 +28,17 @@ from .exceptions import *
|
|||
|
||||
|
||||
class SlicingProfile(object):
|
||||
"""
|
||||
A wrapper for slicing profiles, both meta data and actual profile data.
|
||||
|
||||
Arguments:
|
||||
slicer (str): Identifier of the slicer this profile belongs to.
|
||||
name (str): Identifier of this slicing profile.
|
||||
data (object): Profile data, actual structure depends on individual slicer implementation.
|
||||
display_name (str): Displayable name for this slicing profile.
|
||||
description (str): Description of this slicing profile.
|
||||
"""
|
||||
|
||||
def __init__(self, slicer, name, data, display_name=None, description=None):
|
||||
self.slicer = slicer
|
||||
self.name = name
|
||||
|
|
@ -24,6 +48,27 @@ class SlicingProfile(object):
|
|||
|
||||
|
||||
class TemporaryProfile(object):
|
||||
"""
|
||||
A wrapper for a temporary slicing profile to be used for a slicing job, based on a :class:`SlicingProfile` with
|
||||
optional ``overrides`` applied through the supplied ``save_profile`` method.
|
||||
|
||||
Usage example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
temporary = TemporaryProfile(my_slicer.save_slicer_profile, my_default_profile,
|
||||
overrides=my_overrides)
|
||||
with (temporary) as profile_path:
|
||||
my_slicer.do_slice(..., profile_path=profile_path, ...)
|
||||
|
||||
Arguments:
|
||||
save_profile (callable): Method to use for saving the temporary profile, also responsible for applying the
|
||||
supplied ``overrides``. This will be called according to the method signature of
|
||||
:meth:`~octoprint.plugin.SlicerPlugin.save_slicer_profile`.
|
||||
profile (SlicingProfile): The profile from which to derive the temporary profile.
|
||||
overrides (dict): Optional overrides to apply to the ``profile`` for creation of the temporary profile.
|
||||
"""
|
||||
|
||||
def __init__(self, save_profile, profile, overrides=None):
|
||||
self.save_profile = save_profile
|
||||
self.profile = profile
|
||||
|
|
@ -47,6 +92,15 @@ class TemporaryProfile(object):
|
|||
|
||||
|
||||
class SlicingManager(object):
|
||||
"""
|
||||
The :class:`SlicingManager` is responsible for managing available slicers and slicing profiles.
|
||||
|
||||
Arguments:
|
||||
profile_path (str): Absolute path to the base folder where all slicing profiles are stored.
|
||||
printer_profile_manager (~octoprint.printer.profile.PrinterProfileManager): :class:`~octoprint.printer.profile.PrinterProfileManager`
|
||||
instance to use for accessing available printer profiles, most importantly the currently selected one.
|
||||
"""
|
||||
|
||||
def __init__(self, profile_path, printer_profile_manager):
|
||||
self._profile_path = profile_path
|
||||
self._printer_profile_manager = printer_profile_manager
|
||||
|
|
@ -54,37 +108,55 @@ class SlicingManager(object):
|
|||
self._slicers = dict()
|
||||
self._slicer_names = dict()
|
||||
|
||||
self._progress_callbacks = []
|
||||
self._last_progress_report = None
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Initializes the slicing manager by loading and initializing all available
|
||||
:class:`~octoprint.plugin.SlicerPlugin` implementations.
|
||||
"""
|
||||
self._load_slicers()
|
||||
|
||||
def register_progress_callback(self, callback):
|
||||
self._progress_callbacks.append(callback)
|
||||
|
||||
def unregister_progress_callback(self, callback):
|
||||
self._progress_callbacks.remove(callback)
|
||||
|
||||
def _load_slicers(self):
|
||||
"""
|
||||
Retrieves all registered :class:`~octoprint.plugin.SlicerPlugin` implementations and registers them as
|
||||
available slicers.
|
||||
"""
|
||||
plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin)
|
||||
for name, plugin in plugins.items():
|
||||
self._slicers[plugin.get_slicer_properties()["type"]] = plugin
|
||||
|
||||
@property
|
||||
def slicing_enabled(self):
|
||||
"""
|
||||
Returns:
|
||||
boolean: True if there is at least one configured slicer available, False otherwise.
|
||||
"""
|
||||
return len(self.configured_slicers) > 0
|
||||
|
||||
@property
|
||||
def registered_slicers(self):
|
||||
"""
|
||||
Returns:
|
||||
list of str: Identifiers of all available slicers.
|
||||
"""
|
||||
return self._slicers.keys()
|
||||
|
||||
@property
|
||||
def configured_slicers(self):
|
||||
"""
|
||||
Returns:
|
||||
list of str: Identifiers of all available configured slicers.
|
||||
"""
|
||||
return map(lambda slicer: slicer.get_slicer_properties()["type"], filter(lambda slicer: slicer.is_slicer_configured(), self._slicers.values()))
|
||||
|
||||
@property
|
||||
def default_slicer(self):
|
||||
"""
|
||||
Retrieves the default slicer.
|
||||
|
||||
Returns:
|
||||
str: The identifier of the default slicer or ``None`` if the default slicer is not registered in the
|
||||
system.
|
||||
"""
|
||||
slicer_name = settings().get(["slicing", "defaultSlicer"])
|
||||
if slicer_name in self.registered_slicers:
|
||||
return slicer_name
|
||||
|
|
@ -92,11 +164,90 @@ class SlicingManager(object):
|
|||
return None
|
||||
|
||||
def get_slicer(self, slicer, require_configured=True):
|
||||
if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()):
|
||||
return self._slicers[slicer]
|
||||
raise SlicerNotConfigured(slicer)
|
||||
"""
|
||||
Retrieves the slicer named ``slicer``. If ``require_configured`` is set to True (the default) an exception
|
||||
will be raised if the slicer is not yet configured.
|
||||
|
||||
Arguments:
|
||||
slicer (str): Identifier of the slicer to return
|
||||
require_configured (boolean): Whether to raise an exception if the slicer has not been configured yet (True,
|
||||
the default), or also return an unconfigured slicer (False).
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The ``slicer`` is unknown.
|
||||
~octoprint.slicing.exceptions.SlicerNotConfigured: The ``slicer`` is not yet configured and ``require_configured`` was set to True.
|
||||
"""
|
||||
|
||||
if not slicer in self._slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
if require_configured and not self._slicers[slicer].is_slicer_configured():
|
||||
raise SlicerNotConfigured(slicer)
|
||||
|
||||
return self._slicers[slicer]
|
||||
|
||||
def slice(self, slicer_name, source_path, dest_path, profile_name, callback,
|
||||
callback_args=None, callback_kwargs=None, overrides=None,
|
||||
on_progress=None, on_progress_args=None, on_progress_kwargs=None, printer_profile_id=None, position=None):
|
||||
"""
|
||||
Slices ``source_path`` to ``dest_path`` using slicer ``slicer_name`` and slicing profile ``profile_name``.
|
||||
Since slicing happens asynchronously, ``callback`` will be called when slicing has finished (either successfully
|
||||
or not), with ``callback_args`` and ``callback_kwargs`` supplied.
|
||||
|
||||
If ``callback_args`` is left out, an empty argument list will be assumed for the callback. If ``callback_kwargs``
|
||||
is left out, likewise an empty keyword argument list will be assumed for the callback. Note that in any case
|
||||
the callback *must* support being called with the following optional keyword arguments:
|
||||
|
||||
_analysis
|
||||
If the slicer returned analysis data of the created machine code as part of its slicing result, this keyword
|
||||
argument will contain that data.
|
||||
_error
|
||||
If there was an error while slicing this keyword argument will contain the error message as returned from
|
||||
the slicer.
|
||||
_cancelled
|
||||
If the slicing job was cancelled this keyword argument will be set to True.
|
||||
|
||||
Additionally callees may specify ``overrides`` for the specified slicing profile, e.g. a different extrusion
|
||||
temperature than defined in the profile or a different layer height.
|
||||
|
||||
With ``on_progress``, ``on_progress_args`` and ``on_progress_kwargs``, callees may specify a callback plus
|
||||
arguments and keyword arguments to call upon progress reports from the slicing job. The progress callback will
|
||||
be called with a keyword argument ``_progress`` containing the current slicing progress as a value between 0
|
||||
and 1 plus all additionally specified args and kwargs.
|
||||
|
||||
If a different printer profile than the currently selected one is to be used for slicing, its id can be provided
|
||||
via the keyword argument ``printer_profile_id``.
|
||||
|
||||
If the ``source_path`` is to be a sliced at a different position than the print bed center, this ``position`` can
|
||||
be supplied as a dictionary defining the ``x`` and ``y`` coordinate in print bed coordinates of the model's center.
|
||||
|
||||
Arguments:
|
||||
slicer_name (str): The identifier of the slicer to use for slicing.
|
||||
source_path (str): The absolute path to the source file to slice.
|
||||
dest_path (str): The absolute path to the destination file to slice to.
|
||||
profile_name (str): The name of the slicing profile to use.
|
||||
callback (callable): A callback to call after slicing has finished.
|
||||
callback_args (list or tuple): Arguments of the callback to call after slicing has finished. Defaults to
|
||||
an empty list.
|
||||
callback_kwargs (dict): Keyword arguments for the callback to call after slicing has finished, will be
|
||||
extended by ``_analysis``, ``_error`` or ``_cancelled`` as described above! Defaults to an empty
|
||||
dictionary.
|
||||
overrides (dict): Overrides for the printer profile to apply.
|
||||
on_progress (callable): Callback to call upon slicing progress.
|
||||
on_progress_args (list or tuple): Arguments of the progress callback. Defaults to an empty list.
|
||||
on_progress_kwargs (dict): Keyword arguments of the progress callback, will be extended by ``_progress``
|
||||
as described above! Defaults to an empty dictionary.
|
||||
printer_profile_id (str): Identifier of the printer profile for which to slice, if another than the
|
||||
one currently selected is to be used.
|
||||
position (dict): Dictionary containing the ``x`` and ``y`` coordinate in the print bed's coordinate system
|
||||
of the sliced model's center. If not provided the model will be positioned at the print bed's center.
|
||||
Example: ``dict(x=10,y=20)``.
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer specified via ``slicer_name`` is unknown.
|
||||
~octoprint.slicing.exceptions.SlicerNotConfigured: The slice specified via ``slicer_name`` is not configured yet.
|
||||
"""
|
||||
|
||||
def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None, printer_profile_id=None, position=None):
|
||||
if callback_args is None:
|
||||
callback_args = ()
|
||||
if callback_kwargs is None:
|
||||
|
|
@ -125,7 +276,7 @@ class SlicingManager(object):
|
|||
def slicer_worker(slicer, model_path, machinecode_path, profile_name, overrides, printer_profile, position, callback, callback_args, callback_kwargs):
|
||||
try:
|
||||
slicer_name = slicer.get_slicer_properties()["type"]
|
||||
with self.temporary_profile(slicer_name, name=profile_name, overrides=overrides) as profile_path:
|
||||
with self._temporary_profile(slicer_name, name=profile_name, overrides=overrides) as profile_path:
|
||||
ok, result = slicer.do_slice(
|
||||
model_path,
|
||||
printer_profile,
|
||||
|
|
@ -151,16 +302,41 @@ class SlicingManager(object):
|
|||
args=(slicer, source_path, dest_path, profile_name, overrides, printer_profile, position, callback, callback_args, callback_kwargs))
|
||||
slicer_worker_thread.daemon = True
|
||||
slicer_worker_thread.start()
|
||||
return True, None
|
||||
|
||||
def cancel_slicing(self, slicer_name, source_path, dest_path):
|
||||
if not slicer_name in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer_name)
|
||||
"""
|
||||
Cancels the slicing job on slicer ``slicer_name`` from ``source_path`` to ``dest_path``.
|
||||
|
||||
Arguments:
|
||||
slicer_name (str): Identifier of the slicer on which to cancel the job.
|
||||
source_path (str): The absolute path to the source file being sliced.
|
||||
dest_path (str): The absolute path to the destination file being sliced to.
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer specified via ``slicer_name`` is unknown.
|
||||
"""
|
||||
|
||||
slicer = self.get_slicer(slicer_name)
|
||||
slicer.cancel_slicing(dest_path)
|
||||
|
||||
def load_profile(self, slicer, name, require_configured=True):
|
||||
"""
|
||||
Loads the slicing profile for ``slicer`` with the given profile ``name`` and returns it. If it can't be loaded
|
||||
due to an :class:`IOError` ``None`` will be returned instead.
|
||||
|
||||
If ``require_configured`` is True (the default) a :class:`SlicerNotConfigured` exception will be raised
|
||||
if the indicated ``slicer`` has not yet been configured.
|
||||
|
||||
Returns:
|
||||
SlicingProfile: The requested slicing profile or None if it could not be loaded.
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer specified via ``slicer`` is unknown.
|
||||
~octoprint.slicing.exceptions.SlicerNotConfigured: The slicer specified via ``slicer`` has not yet been configured and
|
||||
``require_configured`` was True.
|
||||
~octoprint.slicing.exceptions.UnknownProfile: The profile for slicer ``slicer`` named ``name`` does not exist.
|
||||
"""
|
||||
|
||||
if not slicer in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
|
|
@ -171,6 +347,38 @@ class SlicingManager(object):
|
|||
return self._load_profile_from_path(slicer, path, require_configured=require_configured)
|
||||
|
||||
def save_profile(self, slicer, name, profile, overrides=None, allow_overwrite=True, display_name=None, description=None):
|
||||
"""
|
||||
Saves the slicer profile ``profile`` for slicer ``slicer`` under name ``name``.
|
||||
|
||||
``profile`` may be either a :class:`SlicingProfile` or a :class:`dict`.
|
||||
|
||||
If it's a :class:`SlicingProfile`, its :attr:`~SlicingProfile.slicer``, :attr:`~SlicingProfile.name` and - if
|
||||
provided - :attr:`~SlicingProfile.display_name` and :attr:`~SlicingProfile.description` attributes will be
|
||||
overwritten with the supplied values.
|
||||
|
||||
If it's a :class:`dict`, a new :class:`SlicingProfile` instance will be created with the supplied meta data and
|
||||
the profile data as the :attr:`~SlicingProfile.data` attribute.
|
||||
|
||||
Arguments:
|
||||
slicer (str): Identifier of the slicer for which to save the ``profile``.
|
||||
name (str): Identifier under which to save the ``profile``.
|
||||
profile (SlicingProfile or dict): The :class:`SlicingProfile` or a :class:`dict` containing the profile
|
||||
data of the profile the save.
|
||||
overrides (dict): Overrides to apply to the ``profile`` before saving it.
|
||||
allow_overwrite (boolean): If True (default) if a profile for the same ``slicer`` of the same ``name``
|
||||
already exists, it will be overwritten. Otherwise an exception will be thrown.
|
||||
display_name (str): The name to display to the user for the profile.
|
||||
description (str): A description of the profile.
|
||||
|
||||
Returns:
|
||||
SlicingProfile: The saved profile (including the applied overrides).
|
||||
|
||||
Raises:
|
||||
ValueError: The supplied ``profile`` is neither a :class:`SlicingProfile` nor a :class:`dict`.
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer ``slicer`` is unknown.
|
||||
~octoprint.slicing.exceptions.ProfileAlreadyExists: A profile with name ``name`` already exists for ``slicer`` and ``allow_overwrite`` is
|
||||
False.
|
||||
"""
|
||||
if not slicer in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
|
|
@ -178,7 +386,7 @@ class SlicingManager(object):
|
|||
if isinstance(profile, dict):
|
||||
profile = SlicingProfile(slicer, name, profile, display_name=display_name, description=description)
|
||||
else:
|
||||
raise ValueError("profile must be a SlicingProfile")
|
||||
raise ValueError("profile must be a SlicingProfile or a dict")
|
||||
else:
|
||||
profile.slicer = slicer
|
||||
profile.name = name
|
||||
|
|
@ -191,7 +399,7 @@ class SlicingManager(object):
|
|||
self._save_profile_to_path(slicer, path, profile, overrides=overrides, allow_overwrite=allow_overwrite)
|
||||
return profile
|
||||
|
||||
def temporary_profile(self, slicer, name=None, overrides=None):
|
||||
def _temporary_profile(self, slicer, name=None, overrides=None):
|
||||
if not slicer in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
|
|
@ -199,27 +407,59 @@ class SlicingManager(object):
|
|||
if name:
|
||||
try:
|
||||
profile = self.load_profile(slicer, name)
|
||||
except IOError:
|
||||
profile = self._get_default_profile(slicer)
|
||||
else:
|
||||
if profile is None:
|
||||
profile = self._get_default_profile(slicer)
|
||||
except (UnknownProfile, IOError):
|
||||
# in that case we'll use the default profile
|
||||
pass
|
||||
|
||||
return TemporaryProfile(self.get_slicer(slicer).save_slicer_profile, profile, overrides=overrides)
|
||||
|
||||
def delete_profile(self, slicer, name):
|
||||
"""
|
||||
Deletes the profile ``name`` for the specified ``slicer``.
|
||||
|
||||
If the profile does not exist, nothing will happen.
|
||||
|
||||
Arguments:
|
||||
slicer (str): Identifier of the slicer for which to delete the profile.
|
||||
name (str): Identifier of the profile to delete.
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer ``slicer`` is unknown.
|
||||
"""
|
||||
|
||||
if not slicer in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
if not name:
|
||||
raise ValueError("name must be set")
|
||||
|
||||
path = self.get_profile_path(slicer, name)
|
||||
if not os.path.exists(path) or not os.path.isfile(path):
|
||||
try:
|
||||
path = self.get_profile_path(slicer, name)
|
||||
except UnknownProfile:
|
||||
return
|
||||
os.remove(path)
|
||||
|
||||
def all_profiles(self, slicer, require_configured=False):
|
||||
"""
|
||||
Retrieves all profiles for slicer ``slicer``.
|
||||
|
||||
If ``require_configured`` is set to True (default is False), only will return the profiles if the ``slicer``
|
||||
is already configured, otherwise a :class:`SlicerNotConfigured` exception will be raised.
|
||||
|
||||
Arguments:
|
||||
slicer (str): Identifier of the slicer for which to retrieve all slicer profiles
|
||||
require_configured (boolean): Whether to require the slicer ``slicer`` to be already configured (True)
|
||||
or not (False, default). If False and the slicer is not yet configured, a :class:`~octoprint.slicing.exceptions.SlicerNotConfigured`
|
||||
exception will be raised.
|
||||
|
||||
Returns:
|
||||
list of SlicingProfile: A list of all :class:`SlicingProfile` instances available for the slicer ``slicer``.
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer ``slicer`` is unknown.
|
||||
~octoprint.slicing.exceptions.SlicerNotConfigured: The slicer ``slicer`` is not configured and ``require_configured`` was True.
|
||||
"""
|
||||
|
||||
if not slicer in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
if require_configured and not slicer in self.configured_slicers:
|
||||
|
|
@ -239,6 +479,19 @@ class SlicingManager(object):
|
|||
return profiles
|
||||
|
||||
def get_slicer_profile_path(self, slicer):
|
||||
"""
|
||||
Retrieves the path where the profiles for slicer ``slicer`` are stored.
|
||||
|
||||
Arguments:
|
||||
slicer (str): Identifier of the slicer for which to retrieve the path.
|
||||
|
||||
Returns:
|
||||
str: The absolute path to the folder where the slicer's profiles are stored.
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer ``slicer`` is unknown.
|
||||
"""
|
||||
|
||||
if not slicer in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
|
|
@ -248,6 +501,25 @@ class SlicingManager(object):
|
|||
return path
|
||||
|
||||
def get_profile_path(self, slicer, name, must_exist=False):
|
||||
"""
|
||||
Retrieves the path to the profile named ``name`` for slicer ``slicer``.
|
||||
|
||||
If ``must_exist`` is set to True (defaults to False) a :class:`UnknownProfile` exception will be raised if the
|
||||
profile doesn't exist yet.
|
||||
|
||||
Arguments:
|
||||
slicer (str): Identifier of the slicer to which the profile belongs to.
|
||||
name (str): Identifier of the profile for which to retrieve the path.
|
||||
must_exist (boolean): Whether the path must exist (True) or not (False, default).
|
||||
|
||||
Returns:
|
||||
str: The absolute path to the profile identified by ``name`` for slicer ``slicer``.
|
||||
|
||||
Raises:
|
||||
~octoprint.slicing.exceptions.UnknownSlicer: The slicer ``slicer`` is unknown.
|
||||
~octoprint.slicing.exceptions.UnknownProfile: The profile named ``name`` doesn't exist and ``must_exist`` was True.
|
||||
"""
|
||||
|
||||
if not slicer in self.registered_slicers:
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
|
|
@ -260,7 +532,7 @@ class SlicingManager(object):
|
|||
if not os.path.realpath(path).startswith(self._profile_path):
|
||||
raise IOError("Path to profile {name} tried to break out of allows sub path".format(**locals()))
|
||||
if must_exist and not (os.path.exists(path) and os.path.isfile(path)):
|
||||
raise IOError("Profile {name} doesn't exist".format(**locals()))
|
||||
raise UnknownProfile(slicer, name)
|
||||
return path
|
||||
|
||||
def _sanitize(self, name):
|
||||
|
|
@ -287,7 +559,8 @@ class SlicingManager(object):
|
|||
if default_profiles and slicer in default_profiles:
|
||||
try:
|
||||
return self.load_profile(slicer, default_profiles[slicer])
|
||||
except:
|
||||
except (UnknownProfile, IOError):
|
||||
# in that case we'll use the slicers predefined default profile
|
||||
pass
|
||||
|
||||
return self.get_slicer(slicer).get_slicer_default_profile()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,31 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Slicing related exceptions.
|
||||
|
||||
.. autoclass:: SlicingException
|
||||
|
||||
.. autoclass:: SlicingCancelled
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: SlicerException
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: UnknownSlicer
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: SlicerNotConfigured
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: ProfileException
|
||||
|
||||
.. autoclass:: UnknownProfile
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: ProfileAlreadyExists
|
||||
:show-inheritance:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
|
|
@ -7,22 +34,74 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms
|
|||
|
||||
|
||||
class SlicingException(BaseException):
|
||||
"""
|
||||
Base exception of all slicing related exceptions.
|
||||
"""
|
||||
pass
|
||||
|
||||
class SlicingCancelled(SlicingException):
|
||||
"""
|
||||
Raised if a slicing job was cancelled.
|
||||
"""
|
||||
pass
|
||||
|
||||
class SlicerException(SlicingException):
|
||||
"""
|
||||
Base exception of all slicer related exceptions.
|
||||
|
||||
.. attribute:: slicer
|
||||
|
||||
Identifier of the slicer for which the exception was raised.
|
||||
"""
|
||||
def __init__(self, slicer, *args, **kwargs):
|
||||
super(SlicingException, self).__init__(*args, **kwargs)
|
||||
self.slicer = slicer
|
||||
|
||||
class SlicerNotConfigured(SlicerException):
|
||||
"""
|
||||
Raised if a slicer is not yet configured but must be configured to proceed.
|
||||
"""
|
||||
def __init__(self, slicer, *args, **kwargs):
|
||||
super(SlicerException, self).__init__(slicer, *args, **kwargs)
|
||||
self.message = "Slicer not configured: {slicer}".format(slicer=slicer)
|
||||
|
||||
class UnknownSlicer(SlicerException):
|
||||
"""
|
||||
Raised if a slicer is unknown.
|
||||
"""
|
||||
def __init__(self, slicer, *args, **kwargs):
|
||||
super(SlicerException, self).__init__(slicer, *args, **kwargs)
|
||||
self.message = "No such slicer: {slicer}".format(slicer=slicer)
|
||||
|
||||
class ProfileException(BaseException):
|
||||
"""
|
||||
Base exception of all slicing profile related exceptions.
|
||||
|
||||
.. attribute:: slicer
|
||||
|
||||
Identifier of the slicer to which the profile belongs.
|
||||
|
||||
.. attribute:: profile
|
||||
|
||||
Identifier of the profile for which the exception was raised.
|
||||
"""
|
||||
def __init__(self, slicer, profile, *args, **kwargs):
|
||||
super(BaseException, self).__init__(*args, **kwargs)
|
||||
self.slicer = slicer
|
||||
self.profile = profile
|
||||
|
||||
class UnknownProfile(ProfileException):
|
||||
"""
|
||||
Raised if a slicing profile does not exist but must exist to proceed.
|
||||
"""
|
||||
def __init__(self, slicer, profile, *args, **kwargs):
|
||||
super(ProfileException, self).__init__(slicer, profile, *args, **kwargs)
|
||||
self.message = "Profile {profile} for slicer {slicer} does not exist".format(profile=profile, slicer=slicer)
|
||||
|
||||
class ProfileAlreadyExists(ProfileException):
|
||||
"""
|
||||
Raised if a slicing profile already exists and must not be overwritten.
|
||||
"""
|
||||
def __init__(self, slicer, profile, *args, **kwargs):
|
||||
super(ProfileException, self).__init__(slicer, profile, *args, **kwargs)
|
||||
self.message = "Profile {profile} for slicer {slicer} already exists".format(profile=profile, slicer=slicer)
|
||||
Loading…
Reference in a new issue