Lots of documentation for slicing related things and some refactorings

This commit is contained in:
Gina Häußge 2015-03-19 20:58:24 +01:00
parent b17730137a
commit f2eeb50381
10 changed files with 655 additions and 140 deletions

View file

@ -11,4 +11,5 @@ Internal Modules
plugin.rst plugin.rst
printer.rst printer.rst
settings.rst settings.rst
slicing.rst
util.rst util.rst

13
docs/modules/slicing.rst Normal file
View file

@ -0,0 +1,13 @@
.. _sec-modules-slicing:
octoprint.slicing
-----------------
.. automodule:: octoprint.slicing
.. _sec-modules-slicing-exceptions:
octoprint.slicing.exceptions
----------------------------
.. automodule:: octoprint.slicing.exceptions

View file

@ -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) 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) args = (source_location, source_path, temp_path, dest_location, dest_path, start_time, printer_profile_id, callback, callback_args)
return self._slicing_manager.slice( self._slicing_manager.slice(slicer_name,
slicer_name, absolute_source_path,
absolute_source_path, temp_path,
temp_path, profile,
profile, stlProcessed,
stlProcessed, position=position,
position=position, callback_args=args,
callback_args=args, overrides=overrides,
overrides=overrides, printer_profile_id=printer_profile_id,
printer_profile_id=printer_profile_id, on_progress=self.on_slicing_progress,
on_progress=self.on_slicing_progress, on_progress_args=(slicer_name, source_location, source_path, dest_location, dest_path))
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): def on_slicing_progress(self, slicer, source_location, source_path, dest_location, dest_path, _progress=None):
if not _progress: if not _progress:

View file

@ -18,11 +18,40 @@ import octoprint.util.gcodeInterpreter as gcodeInterpreter
class QueueEntry(collections.namedtuple("QueueEntry", "path, type, location, absolute_path, printer_profile")): 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): def __str__(self):
return "{location}:{path}".format(location=self.location, path=self.path) return "{location}:{path}".format(location=self.location, path=self.path)
class AnalysisQueue(object): 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): def __init__(self):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
self._callbacks = [] self._callbacks = []
@ -56,6 +85,21 @@ class AnalysisQueue(object):
eventManager().fire(Events.METADATA_ANALYSIS_FINISHED, {"file": entry.path, "result": result}) eventManager().fire(Events.METADATA_ANALYSIS_FINISHED, {"file": entry.path, "result": result})
class AbstractAnalysisQueue(object): 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): def __init__(self, finished_callback):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -75,6 +119,18 @@ class AbstractAnalysisQueue(object):
self._worker.start() self._worker.start()
def enqueue(self, entry, high_priority=False): 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: if high_priority:
self._logger.debug("Adding entry {entry} to analysis queue with high priority".format(entry=entry)) self._logger.debug("Adding entry {entry} to analysis queue with high priority".format(entry=entry))
prio = 0 prio = 0
@ -85,6 +141,10 @@ class AbstractAnalysisQueue(object):
self._queue.put((prio, entry)) self._queue.put((prio, entry))
def pause(self): def pause(self):
"""
Pauses processing of the queue, e.g. when a print is active.
"""
self._logger.debug("Pausing analysis") self._logger.debug("Pausing analysis")
self._active.clear() self._active.clear()
if self._current is not None: if self._current is not None:
@ -92,6 +152,10 @@ class AbstractAnalysisQueue(object):
self._do_abort() self._do_abort()
def resume(self): def resume(self):
"""
Resumes processing of the queue, e.g. when a print has finished.
"""
self._logger.debug("Resuming analyzer") self._logger.debug("Resuming analyzer")
self._active.set() self._active.set()
@ -134,13 +198,42 @@ class AbstractAnalysisQueue(object):
self._current_progress = None self._current_progress = None
def _do_analysis(self): 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 return None
def _do_abort(self): def _do_abort(self):
"""
Aborts analysis of the current entry. Needs to be overridden by sub classes.
"""
pass pass
class GcodeAnalysisQueue(AbstractAnalysisQueue): 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): def _do_analysis(self):
try: try:

View file

@ -869,9 +869,10 @@ class EventHandlerPlugin(OctoPrintPlugin):
""" """
Called by OctoPrint upon processing of a fired event on the platform. 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>` Arguments:
for possible values event (str): The type of event that got fired, see :ref:`the list of events <sec-events-available_events>`
:param dict payload: the payload as provided with the event for possible values
payload (dict): The payload as provided with the event
""" """
pass pass
@ -903,10 +904,14 @@ class SlicerPlugin(OctoPrintPlugin):
name name
The human readable name of the slicer. This will be displayed to the user during slicer selection. The human readable name of the slicer. This will be displayed to the user during slicer selection.
same_device same_device
``True`` if the slicer runs on the same device as OctoPrint, ``False`` otherwise. Slicers running on the same True if the slicer runs on the same device as OctoPrint, False otherwise. Slicers running on the same
device will TODO 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 progress_report
``True`` if the slicer can report back slicing progress to OctoPrint ``False`` otherwise. ``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( return dict(
type=None, type=None,
@ -917,38 +922,48 @@ class SlicerPlugin(OctoPrintPlugin):
def get_slicer_default_profile(self): def get_slicer_default_profile(self):
""" """
Should return a :class:`SlicingProfile` containing the default slicing profile to use with this slicer if Should return a :class:`~octoprint.slicing.SlicingProfile` containing the default slicing profile to use with
no other profile has been selected. 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 return None
def get_slicer_profile(self, path): 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 return None
def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=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 Should save the provided :class:`~octoprint.slicing.SlicingProfile` to the indicated ``path``, after applying
``overrides``. If a profile is already saved under the indicated path and ``allow_overwrite`` is set to any supplied ``overrides``. If a profile is already saved under the indicated path and ``allow_overwrite`` is
``False`` (defaults to ``True``), an ``IOError`` should be raised. set to False (defaults to True), an :class:`IOError` should be raised.
:param string path: the path to which to save the profile Arguments:
:param SlicingProfile profile: the profile to save path (str): The absolute path to which to save the profile.
:param bool allow_overwrite: whether to allow to overwrite an existing profile at the indicated path (``True``, default) profile (SlicingProfile): The profile to save.
or not (``False``) - if a profile already exists on the path and this is ``False`` allow_overwrite (boolean): Whether to allow to overwrite an existing profile at the indicated path (True,
and :class:`IOError` should be raised default) or not (False). If a profile already exists on teh path and this is False an
:param dict overrides: profile overrides to apply to the ``profile`` before saving it :class:`IOError` should be raised.
overrides (dict): Profile overrides to apply to the ``profile`` before saving it
""" """
pass 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): 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``, 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 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. 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``, 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. 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 pass
def cancel_slicing(self, machinecode_path): 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 pass

View file

@ -106,12 +106,15 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
from octoprint.server.api import valid_boolean_trues from octoprint.server.api import valid_boolean_trues
profile_allow_overwrite = flask.request.values["allowOverwrite"] in valid_boolean_trues profile_allow_overwrite = flask.request.values["allowOverwrite"] in valid_boolean_trues
slicingManager.save_profile("cura", try:
profile_name, slicingManager.save_profile("cura",
profile_dict, profile_name,
allow_overwrite=profile_allow_overwrite, profile_dict,
display_name=profile_display_name, allow_overwrite=profile_allow_overwrite,
description=profile_description) 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( result = dict(
resource=flask.url_for("api.slicingGetSlicerProfile", slicer="cura", name=profile_name, _external=True), 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) 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): 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) new_profile = Profile.merge_profile(profile.data, overrides=overrides)
if profile.display_name is not None: if profile.display_name is not None:
@ -388,9 +394,6 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
return profile_dict return profile_dict
def _save_profile(self, path, profile, allow_overwrite=True): 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 import yaml
with open(path, "wb") as f: with open(path, "wb") as f:
yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True) yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True)

View file

@ -15,6 +15,7 @@ from octoprint.server.util.flask import restricted_access, get_json_command_from
from octoprint.server.api import api from octoprint.server.api import api
from octoprint.events import Events from octoprint.events import Events
import octoprint.filemanager import octoprint.filemanager
import octoprint.slicing
#~~ GCODE file handling #~~ GCODE file handling
@ -297,17 +298,20 @@ def gcodeFileCommand(filename, target):
printer.select_file(filenameToSelect, sd, printAfterLoading) printer.select_file(filenameToSelect, sd, printAfterLoading)
elif command == "slice": elif command == "slice":
if "slicer" in data.keys(): try:
slicer = data["slicer"] if "slicer" in data:
del data["slicer"] slicer = data["slicer"]
if not slicer in slicingManager.registered_slicers: del data["slicer"]
return make_response("Slicer {slicer} is not available".format(**locals()), 400) slicer_instance = slicingManager.get_slicer(slicer)
slicer_instance = slicingManager.get_slicer(slicer)
elif "cura" in slicingManager.registered_slicers: elif "cura" in slicingManager.registered_slicers:
slicer = "cura" slicer = "cura"
slicer_instance = slicingManager.get_slicer("cura") slicer_instance = slicingManager.get_slicer("cura")
else:
return make_response("Cannot slice {filename}, no slicer available".format(**locals()), 415) 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"): if not octoprint.filemanager.valid_file_type(filename, type="stl"):
return make_response("Cannot slice {filename}, not an STL file".format(**locals()), 415) 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 # 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) 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"] gcode_name = data["gcode"]
del data["gcode"] del data["gcode"]
else: else:
@ -374,31 +378,31 @@ def gcodeFileCommand(filename, target):
filenameToSelect = fileManager.path_on_disk(target, gcode_name) filenameToSelect = fileManager.path_on_disk(target, gcode_name)
printer.select_file(filenameToSelect, sd, print_after_slicing) printer.select_file(filenameToSelect, sd, print_after_slicing)
ok, result = fileManager.slice(slicer, target, filename, target, gcode_name, try:
profile=profile, fileManager.slice(slicer, target, filename, target, gcode_name,
printer_profile_id=printerProfile, profile=profile,
position=position, printer_profile_id=printerProfile,
overrides=overrides, position=position,
callback=slicing_done, overrides=overrides,
callback_args=(target, gcode_name, select_after_slicing, print_after_slicing)) 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 = {}
files = {} location = url_for(".readGcodeFile", target=target, filename=gcode_name, _external=True)
location = url_for(".readGcodeFile", target=target, filename=gcode_name, _external=True) result = {
result = { "name": gcode_name,
"name": gcode_name, "origin": FileDestinations.LOCAL,
"origin": FileDestinations.LOCAL, "refs": {
"refs": { "resource": location,
"resource": location, "download": url_for("index", _external=True) + "downloads/files/" + target + "/" + gcode_name
"download": url_for("index", _external=True) + "downloads/files/" + target + "/" + gcode_name
}
} }
}
r = make_response(jsonify(result), 202) r = make_response(jsonify(result), 202)
r.headers["Location"] = location r.headers["Location"] = location
return r return r
else:
return make_response("Could not slice: {result}".format(result=result), 500)
return NO_CONTENT return NO_CONTENT

View file

@ -14,7 +14,7 @@ from octoprint.server.api import api, NO_CONTENT
from octoprint.settings import settings as s, valid_boolean_trues 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"]) @api.route("/slicing", methods=["GET"])
@ -28,37 +28,39 @@ def slicingListAll():
result = dict() result = dict()
for slicer in slicers: for slicer in slicers:
slicer_impl = slicingManager.get_slicer(slicer, require_configured=False) try:
result[slicer] = dict( slicer_impl = slicingManager.get_slicer(slicer, require_configured=False)
key=slicer, result[slicer] = dict(
displayName=slicer_impl.get_slicer_properties()["name"], key=slicer,
default=default_slicer == slicer, displayName=slicer_impl.get_slicer_properties()["name"],
configured = slicer_impl.is_slicer_configured(), default=default_slicer == slicer,
profiles=_getSlicingProfilesData(slicer) configured = slicer_impl.is_slicer_configured(),
) profiles=_getSlicingProfilesData(slicer)
)
except (UnknownSlicer, SlicerNotConfigured):
# this should never happen
pass
return jsonify(result) return jsonify(result)
@api.route("/slicing/<string:slicer>/profiles", methods=["GET"]) @api.route("/slicing/<string:slicer>/profiles", methods=["GET"])
def slicingListSlicerProfiles(slicer): def slicingListSlicerProfiles(slicer):
if not slicer in slicingManager.registered_slicers:
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
configured = False configured = False
if "configured" in request.values and request.values["configured"] in valid_boolean_trues: 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 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"]) @api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["GET"])
def slicingGetSlicerProfile(slicer, name): 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) return make_response("Unknown slicer {slicer}".format(**locals()), 404)
except UnknownProfile:
profile = slicingManager.load_profile(slicer, name)
if not profile:
return make_response("Profile not found", 404) return make_response("Profile not found", 404)
result = _getSlicingProfileData(slicer, name, profile) result = _getSlicingProfileData(slicer, name, profile)
@ -68,9 +70,6 @@ def slicingGetSlicerProfile(slicer, name):
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["PUT"]) @api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["PUT"])
@restricted_access @restricted_access
def slicingAddSlicerProfile(slicer, name): 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"]: if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
@ -89,7 +88,12 @@ def slicingAddSlicerProfile(slicer, name):
if "description" in json_data: if "description" in json_data:
description = json_data["description"] 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) result = _getSlicingProfileData(slicer, name, profile)
r = make_response(jsonify(result), 201) r = make_response(jsonify(result), 201)
@ -99,15 +103,15 @@ def slicingAddSlicerProfile(slicer, name):
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["PATCH"]) @api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["PATCH"])
@restricted_access @restricted_access
def slicingPatchSlicerProfile(slicer, name): 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"]: if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content-type JSON", 400) return make_response("Expected content-type JSON", 400)
profile = slicingManager.load_profile(slicer, name) try:
if not profile: profile = slicingManager.load_profile(slicer, name)
return make_response("Profile not found", 404) 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: try:
json_data = request.json json_data = request.json
@ -133,23 +137,26 @@ def slicingPatchSlicerProfile(slicer, name):
s().set(["slicing", "defaultProfiles"], default_profiles) s().set(["slicing", "defaultProfiles"], default_profiles)
s().save(force=True) s().save(force=True)
slicingManager.save_profile(slicer, name, profile, overrides=data, display_name=display_name, description=description) try:
return NO_CONTENT 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"]) @api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["DELETE"])
@restricted_access @restricted_access
def slicingDelSlicerProfile(slicer, name): 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) 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 return NO_CONTENT
def _getSlicingProfilesData(slicer, require_configured=False): def _getSlicingProfilesData(slicer, require_configured=False):
try: profiles = slicingManager.all_profiles(slicer, require_configured=require_configured)
profiles = slicingManager.all_profiles(slicer, require_configured=require_configured)
except SlicerNotConfigured:
return dict()
result = dict() result = dict()
for name, profile in profiles.items(): for name, profile in profiles.items():

View file

@ -1,4 +1,17 @@
# coding=utf-8 # 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 from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>" __author__ = "Gina Häußge <osd@foosel.net>"
@ -15,6 +28,17 @@ from .exceptions import *
class SlicingProfile(object): 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): def __init__(self, slicer, name, data, display_name=None, description=None):
self.slicer = slicer self.slicer = slicer
self.name = name self.name = name
@ -24,6 +48,27 @@ class SlicingProfile(object):
class TemporaryProfile(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): def __init__(self, save_profile, profile, overrides=None):
self.save_profile = save_profile self.save_profile = save_profile
self.profile = profile self.profile = profile
@ -47,6 +92,15 @@ class TemporaryProfile(object):
class SlicingManager(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): def __init__(self, profile_path, printer_profile_manager):
self._profile_path = profile_path self._profile_path = profile_path
self._printer_profile_manager = printer_profile_manager self._printer_profile_manager = printer_profile_manager
@ -54,37 +108,55 @@ class SlicingManager(object):
self._slicers = dict() self._slicers = dict()
self._slicer_names = dict() self._slicer_names = dict()
self._progress_callbacks = []
self._last_progress_report = None
def initialize(self): def initialize(self):
"""
Initializes the slicing manager by loading and initializing all available
:class:`~octoprint.plugin.SlicerPlugin` implementations.
"""
self._load_slicers() 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): 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) plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin)
for name, plugin in plugins.items(): for name, plugin in plugins.items():
self._slicers[plugin.get_slicer_properties()["type"]] = plugin self._slicers[plugin.get_slicer_properties()["type"]] = plugin
@property @property
def slicing_enabled(self): def slicing_enabled(self):
"""
Returns:
boolean: True if there is at least one configured slicer available, False otherwise.
"""
return len(self.configured_slicers) > 0 return len(self.configured_slicers) > 0
@property @property
def registered_slicers(self): def registered_slicers(self):
"""
Returns:
list of str: Identifiers of all available slicers.
"""
return self._slicers.keys() return self._slicers.keys()
@property @property
def configured_slicers(self): 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())) return map(lambda slicer: slicer.get_slicer_properties()["type"], filter(lambda slicer: slicer.is_slicer_configured(), self._slicers.values()))
@property @property
def default_slicer(self): 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"]) slicer_name = settings().get(["slicing", "defaultSlicer"])
if slicer_name in self.registered_slicers: if slicer_name in self.registered_slicers:
return slicer_name return slicer_name
@ -92,11 +164,90 @@ class SlicingManager(object):
return None return None
def get_slicer(self, slicer, require_configured=True): 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] Retrieves the slicer named ``slicer``. If ``require_configured`` is set to True (the default) an exception
raise SlicerNotConfigured(slicer) 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: if callback_args is None:
callback_args = () callback_args = ()
if callback_kwargs is None: 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): def slicer_worker(slicer, model_path, machinecode_path, profile_name, overrides, printer_profile, position, callback, callback_args, callback_kwargs):
try: try:
slicer_name = slicer.get_slicer_properties()["type"] 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( ok, result = slicer.do_slice(
model_path, model_path,
printer_profile, 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)) 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.daemon = True
slicer_worker_thread.start() slicer_worker_thread.start()
return True, None
def cancel_slicing(self, slicer_name, source_path, dest_path): 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 = self.get_slicer(slicer_name)
slicer.cancel_slicing(dest_path) slicer.cancel_slicing(dest_path)
def load_profile(self, slicer, name, require_configured=True): 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: if not slicer in self.registered_slicers:
raise UnknownSlicer(slicer) raise UnknownSlicer(slicer)
@ -171,6 +347,38 @@ class SlicingManager(object):
return self._load_profile_from_path(slicer, path, require_configured=require_configured) 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): 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: if not slicer in self.registered_slicers:
raise UnknownSlicer(slicer) raise UnknownSlicer(slicer)
@ -178,7 +386,7 @@ class SlicingManager(object):
if isinstance(profile, dict): if isinstance(profile, dict):
profile = SlicingProfile(slicer, name, profile, display_name=display_name, description=description) profile = SlicingProfile(slicer, name, profile, display_name=display_name, description=description)
else: else:
raise ValueError("profile must be a SlicingProfile") raise ValueError("profile must be a SlicingProfile or a dict")
else: else:
profile.slicer = slicer profile.slicer = slicer
profile.name = name profile.name = name
@ -191,7 +399,7 @@ class SlicingManager(object):
self._save_profile_to_path(slicer, path, profile, overrides=overrides, allow_overwrite=allow_overwrite) self._save_profile_to_path(slicer, path, profile, overrides=overrides, allow_overwrite=allow_overwrite)
return profile 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: if not slicer in self.registered_slicers:
raise UnknownSlicer(slicer) raise UnknownSlicer(slicer)
@ -199,27 +407,59 @@ class SlicingManager(object):
if name: if name:
try: try:
profile = self.load_profile(slicer, name) profile = self.load_profile(slicer, name)
except IOError: except (UnknownProfile, IOError):
profile = self._get_default_profile(slicer) # in that case we'll use the default profile
else: pass
if profile is None:
profile = self._get_default_profile(slicer)
return TemporaryProfile(self.get_slicer(slicer).save_slicer_profile, profile, overrides=overrides) return TemporaryProfile(self.get_slicer(slicer).save_slicer_profile, profile, overrides=overrides)
def delete_profile(self, slicer, name): 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: if not slicer in self.registered_slicers:
raise UnknownSlicer(slicer) raise UnknownSlicer(slicer)
if not name: if not name:
raise ValueError("name must be set") raise ValueError("name must be set")
path = self.get_profile_path(slicer, name) try:
if not os.path.exists(path) or not os.path.isfile(path): path = self.get_profile_path(slicer, name)
except UnknownProfile:
return return
os.remove(path) os.remove(path)
def all_profiles(self, slicer, require_configured=False): 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: if not slicer in self.registered_slicers:
raise UnknownSlicer(slicer) raise UnknownSlicer(slicer)
if require_configured and not slicer in self.configured_slicers: if require_configured and not slicer in self.configured_slicers:
@ -239,6 +479,19 @@ class SlicingManager(object):
return profiles return profiles
def get_slicer_profile_path(self, slicer): 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: if not slicer in self.registered_slicers:
raise UnknownSlicer(slicer) raise UnknownSlicer(slicer)
@ -248,6 +501,25 @@ class SlicingManager(object):
return path return path
def get_profile_path(self, slicer, name, must_exist=False): 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: if not slicer in self.registered_slicers:
raise UnknownSlicer(slicer) raise UnknownSlicer(slicer)
@ -260,7 +532,7 @@ class SlicingManager(object):
if not os.path.realpath(path).startswith(self._profile_path): 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())) 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)): 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 return path
def _sanitize(self, name): def _sanitize(self, name):
@ -287,7 +559,8 @@ class SlicingManager(object):
if default_profiles and slicer in default_profiles: if default_profiles and slicer in default_profiles:
try: try:
return self.load_profile(slicer, default_profiles[slicer]) return self.load_profile(slicer, default_profiles[slicer])
except: except (UnknownProfile, IOError):
# in that case we'll use the slicers predefined default profile
pass pass
return self.get_slicer(slicer).get_slicer_default_profile() return self.get_slicer(slicer).get_slicer_default_profile()

View file

@ -1,4 +1,31 @@
# coding=utf-8 # 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 from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>" __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): class SlicingException(BaseException):
"""
Base exception of all slicing related exceptions.
"""
pass pass
class SlicingCancelled(SlicingException): class SlicingCancelled(SlicingException):
"""
Raised if a slicing job was cancelled.
"""
pass pass
class SlicerException(SlicingException): 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): def __init__(self, slicer, *args, **kwargs):
super(SlicingException, self).__init__(*args, **kwargs) super(SlicingException, self).__init__(*args, **kwargs)
self.slicer = slicer self.slicer = slicer
class SlicerNotConfigured(SlicerException): class SlicerNotConfigured(SlicerException):
"""
Raised if a slicer is not yet configured but must be configured to proceed.
"""
def __init__(self, slicer, *args, **kwargs): def __init__(self, slicer, *args, **kwargs):
super(SlicerException, self).__init__(slicer, *args, **kwargs) super(SlicerException, self).__init__(slicer, *args, **kwargs)
self.message = "Slicer not configured: {slicer}".format(slicer=slicer) self.message = "Slicer not configured: {slicer}".format(slicer=slicer)
class UnknownSlicer(SlicerException): class UnknownSlicer(SlicerException):
"""
Raised if a slicer is unknown.
"""
def __init__(self, slicer, *args, **kwargs): def __init__(self, slicer, *args, **kwargs):
super(SlicerException, self).__init__(slicer, *args, **kwargs) super(SlicerException, self).__init__(slicer, *args, **kwargs)
self.message = "No such slicer: {slicer}".format(slicer=slicer) 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)