From 82ae52e619ea9175bdf07f224c468cec9fb4b50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 28 Sep 2016 12:53:43 +0200 Subject: [PATCH 1/2] More docs & client fixes --- docs/jsclientlib/socket.rst | 2 +- docs/jsclientlib/timelapse.rst | 67 +++++++++++++++- docs/jsclientlib/users.rst | 79 ++++++++++++++++++- docs/jsclientlib/wizard.rst | 21 ++++- .../static/js/app/client/timelapse.js | 51 ++++++++++-- .../static/js/app/viewmodels/timelapse.js | 2 +- 6 files changed, 207 insertions(+), 15 deletions(-) diff --git a/docs/jsclientlib/socket.rst b/docs/jsclientlib/socket.rst index 21c32d4c..835ba839 100644 --- a/docs/jsclientlib/socket.rst +++ b/docs/jsclientlib/socket.rst @@ -102,7 +102,7 @@ Instructs the server to decrease the message rate by 500ms. -.. sec-jsclient-socket-throttling: +.. _sec-jsclient-socket-throttling: Communication Throttling ======================== diff --git a/docs/jsclientlib/timelapse.rst b/docs/jsclientlib/timelapse.rst index b2879553..5a2e9047 100644 --- a/docs/jsclientlib/timelapse.rst +++ b/docs/jsclientlib/timelapse.rst @@ -3,26 +3,87 @@ :mod:`OctoPrint.timelapse` -------------------------- -.. todo:: +.. js:function:: OctoPrint.timelapse.get(unrendered, opts) - Needs to be documented + Get a list of all timelapses and the current timelapse config. -.. js:function:: OctoPrint.timelapse.get(opts) + If ``unrendered`` is true, also retrieve the list of unrendered + timelapses. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response .. js:function:: OctoPrint.timelapse.list(opts) + Get the lists of rendered and unrendered timelapses. The returned promis + will be resolved with an object containing the properties ``rendered`` + which will have the list of rendered timelapses, and ``unrendered`` which + will have the list of unrendered timelapses. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + +.. js:function:: OctoPrint.timelapse.listRendered(opts) + + Get the list of rendered timelapses. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + +.. js:function:: OctoPrint.timelapse.listUnrendered(opts) + + Get the list of unrendered timelapses. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.timelapse.download(filename, opts) + Download the rendered timelapse ``filename``. + + :param string filename: The name of the rendered timelapse to download + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.timelapse.delete(filename, opts) + Delete the rendered timelapse ``filename``. + + :param string filename: The name of the rendered timelapse to delete + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.timelapse.deleteUnrendered(name, opts) + Delete the unrendered timelapse ``name``. + + :param string name: The name of the unrendered timelapse to delete + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.timelapse.renderUnrendered(name, opts) + Render the unrendered timelapse ``name``. + + :param string name: The name of the unrendered timelapse to render + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.timelapse.getConfig(opts) + Get the current timelapse configuration. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.timelapse.saveConfig(config, opts) + Save the timelapse configuration. + + :param object config: The config to save + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. seealso:: :ref:`Timelapse API ` diff --git a/docs/jsclientlib/users.rst b/docs/jsclientlib/users.rst index 70487563..c86b5d19 100644 --- a/docs/jsclientlib/users.rst +++ b/docs/jsclientlib/users.rst @@ -3,31 +3,104 @@ :mod:`OctoPrint.users` ---------------------- -.. todo:: +.. note:: - Needs to be documented + Most methods here require that the used API token or a the existing browser session + has admin rights *or* corresponds to the user to be modified. Some methods + definitely require admin rights. .. js:function:: OctoPrint.users.list(opts) + Get a list of all registered users. + + Requires admin rights. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.get(name, opts) + Get information about a specific user. + + :param string name: The user's name + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.add(user, opts) + Add a new user. + + Requires admin rights. + + :param object user: The new user + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.update(name, active, admin, opts) + Update an existing user. + + Requires admin rights. + + :param string name: The user's name + :param bool active: The new ``active`` state of the user + :param bool admin: The new ``admin`` state of the user + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.delete(name, opts) + Delete an existing user. + + Requires admin rights. + + :param string name: The user's name + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.changePassword(name, password, opts) + Change the password for a user. + + :param string name: The user's name + :param string password: The new password + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.generateApiKey(name, opts) + Generate a new API key for a user. + + :param string name: The user's name + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.resetApiKey(name, opts) + Reset the API key for a user to being unset. + + :param string name: The user's name + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.getSettings(name, opts) + Get the settings for a user. + + :param string name: The user's name + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.users.saveSettings(name, settings, opts) + Save the settings for a user. + + :param string name: The user's name + :param object settings: The new settings, may be a partial set of settings which will be merged unto the current ones + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. seealso:: - :ref:`User API ` + :ref:`User API ` The documentation of the underlying user API. diff --git a/docs/jsclientlib/wizard.rst b/docs/jsclientlib/wizard.rst index d6398c9f..3a124f94 100644 --- a/docs/jsclientlib/wizard.rst +++ b/docs/jsclientlib/wizard.rst @@ -3,10 +3,27 @@ :mod:`OctoPrint.wizard` ----------------------- -.. todo:: +.. note:: - Needs to be documented + All methods here require that the used API token or a the existing browser session + has admin rights. .. js:function:: OctoPrint.wizard.get(opts) + Retrieve additional data about registered wizards. + + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + .. js:function:: OctoPrint.wizard.finish(handled, opts) + + Inform wizards that the wizard dialog has been finished. + + :param list handled: List of identifiers of handled wizards + :param object opts: Additional options for the request + :returns Promise: A `jQuery Promise `_ for the request's response + +.. seealso:: + + :ref:`Wizard API ` + The documentation of the underlying wizard API. diff --git a/src/octoprint/static/js/app/client/timelapse.js b/src/octoprint/static/js/app/client/timelapse.js index 4bfde8a6..fbbdf1e3 100644 --- a/src/octoprint/static/js/app/client/timelapse.js +++ b/src/octoprint/static/js/app/client/timelapse.js @@ -7,25 +7,52 @@ })(window || this, function(OctoPrint, $) { var url = "api/timelapse"; + var downloadUrl = "downloads/timelapse"; + var timelapseUrl = function(filename) { return url + "/" + filename; }; + var timelapseDownloadUrl = function(filename) { + return downloadUrl + "/" + filename; + }; + var unrenderedTimelapseUrl = function(name) { return url + "/unrendered/" + name; }; - var getTimelapseData = function (opts) { + var getTimelapseData = function (unrendered, opts) { + if (unrendered) { + opts = opts || {}; + opts.data = {unrendered: unrendered}; + } return OctoPrint.get(url, opts); }; OctoPrint.timelapse = { get: getTimelapseData, - list: function (opts) { + list: function(opts) { var deferred = $.Deferred(); - getTimelapseData(opts) + getTimelapseData(true, opts) + .done(function (response, status, request) { + deferred.resolve({ + rendered: response.files, + unrendered: response.unrendered + }, status, request); + }) + .fail(function () { + deferred.reject.apply(null, arguments); + }); + + return deferred.promise(); + }, + + listRendered: function (opts) { + var deferred = $.Deferred(); + + getTimelapseData(false, opts) .done(function (response, status, request) { deferred.resolve(response.files, status, request); }) @@ -36,8 +63,22 @@ return deferred.promise(); }, + listUnrendered: function (opts) { + var deferred = $.Deferred(); + + getTimelapseData(true, opts) + .done(function (response, status, request) { + deferred.resolve(response.unrendered, status, request); + }) + .fail(function () { + deferred.reject.apply(null, arguments); + }); + + return deferred.promise(); + }, + download: function (filename, opts) { - return OctoPrint.download(timelapseUrl(filename), opts); + return OctoPrint.download(timelapseDownloadUrl(filename), opts); }, delete: function (filename, opts) { @@ -54,7 +95,7 @@ getConfig: function (opts) { var deferred = $.Deferred(); - getTimelapseData(opts) + getTimelapseData(false, opts) .done(function (response, status, request) { deferred.resolve(response.config, status, request); }) diff --git a/src/octoprint/static/js/app/viewmodels/timelapse.js b/src/octoprint/static/js/app/viewmodels/timelapse.js index f44ad165..fb1c3604 100644 --- a/src/octoprint/static/js/app/viewmodels/timelapse.js +++ b/src/octoprint/static/js/app/viewmodels/timelapse.js @@ -125,7 +125,7 @@ $(function() { ); self.requestData = function() { - OctoPrint.timelapse.get({ data: { unrendered: true} }) + OctoPrint.timelapse.get(true) .done(self.fromResponse); }; From e038cfa4dc2f26bd5605facd15a5422eaa1cf465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 28 Sep 2016 13:43:15 +0200 Subject: [PATCH 2/2] Track bypassed views on cache too Only keep the cache keys, but that way we know when we were going to cache something and then didn't due to environmental factors, e.g. headers on request or response. Necessary to be able to track if the preliminary caching has been done during startup to properly reflect that on cached.gif --- src/octoprint/server/util/flask.py | 53 +++++++++++++++++++++++------- src/octoprint/server/views.py | 3 ++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 87c198b2..6acb4488 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -467,7 +467,9 @@ class LessSimpleCache(BaseCache): def __init__(self, threshold=500, default_timeout=300): BaseCache.__init__(self, default_timeout=default_timeout) + self._mutex = threading.RLock() self._cache = {} + self._bypassed = set() self.clear = self._cache.clear self._threshold = threshold @@ -476,27 +478,35 @@ class LessSimpleCache(BaseCache): now = time.time() for idx, (key, (expires, _)) in enumerate(self._cache.items()): if expires is not None and expires <= now or idx % 3 == 0: - self._cache.pop(key, None) + with self._mutex: + self._cache.pop(key, None) def get(self, key): import pickle now = time.time() - expires, value = self._cache.get(key, (0, None)) + with self._mutex: + expires, value = self._cache.get(key, (0, None)) if expires is None or expires > now: return pickle.loads(value) def set(self, key, value, timeout=None): import pickle - self._prune() - self._cache[key] = (self.calculate_timeout(timeout=timeout), - pickle.dumps(value, pickle.HIGHEST_PROTOCOL)) + + with self._mutex: + self._prune() + self._cache[key] = (self.calculate_timeout(timeout=timeout), + pickle.dumps(value, pickle.HIGHEST_PROTOCOL)) + if key in self._bypassed: + self._bypassed.remove(key) def add(self, key, value, timeout=None): - self.set(key, value, timeout=None) - self._cache.setdefault(key, self._cache[key]) + with self._mutex: + self.set(key, value, timeout=None) + self._cache.setdefault(key, self._cache[key]) def delete(self, key): - self._cache.pop(key, None) + with self._mutex: + self._cache.pop(key, None) def calculate_timeout(self, timeout=None): if timeout is None: @@ -508,7 +518,8 @@ class LessSimpleCache(BaseCache): def over_threshold(self): if self._threshold is None: return False - return len(self._cache) > self._threshold + with self._mutex: + return len(self._cache) > self._threshold def __getitem__(self, key): return self.get(key) @@ -520,7 +531,16 @@ class LessSimpleCache(BaseCache): return self.delete(key) def __contains__(self, key): - return key in self._cache + with self._mutex: + return key in self._cache + + def set_bypassed(self, key): + with self._mutex: + self._bypassed.add(key) + + def is_bypassed(self, key): + with self._mutex: + return key in self._bypassed _cache = LessSimpleCache() @@ -530,17 +550,20 @@ def cached(timeout=5 * 60, key=lambda: "view:%s" % flask.request.path, unless=No def decorated_function(*args, **kwargs): logger = logging.getLogger(__name__) + cache_key = key() + # bypass the cache if "unless" condition is true if callable(unless) and unless(): logger.debug("Cache for {path} bypassed, calling wrapped function".format(path=flask.request.path)) + _cache.set_bypassed(cache_key) return f(*args, **kwargs) # also bypass the cache if it's disabled completely if not settings().getBoolean(["devel", "cache", "enabled"]): logger.debug("Cache for {path} disabled, calling wrapped function".format(path=flask.request.path)) + _cache.set_bypassed(cache_key) return f(*args, **kwargs) - cache_key = key() rv = _cache.get(cache_key) # only take the value from the cache if we are not required to refresh it from the wrapped function @@ -556,7 +579,8 @@ def cached(timeout=5 * 60, key=lambda: "view:%s" % flask.request.path, unless=No # do not store if the "unless_response" condition is true if callable(unless_response) and unless_response(rv): - logger.debug("Not caching result for {path}, bypassed".format(path=flask.request.path)) + logger.debug("Not caching result for {path} (key: {key}), bypassed".format(path=flask.request.path, key=cache_key)) + _cache.set_bypassed(cache_key) return rv # store it in the cache @@ -573,6 +597,11 @@ def is_in_cache(key=lambda: "view:%s" % flask.request.path): key = key() return key in _cache +def is_cache_bypassed(key=lambda: "view:%s" % flask.request.path): + if callable(key): + key = key() + return _cache.is_bypassed(key) + def cache_check_headers(): return "no-cache" in flask.request.cache_control or "no-cache" in flask.request.pragma diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 5dc664ed..ef1a62a1 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -77,6 +77,9 @@ def in_cache(): elif util.flask.is_in_cache(key): _logger.info("Found path {} in cache (key: {}), signaling as cached".format(path, key)) return response + elif util.flask.is_cache_bypassed(key): + _logger.info("Path {} was bypassed from cache (key: {}), signaling as cached".format(path, key)) + return response else: _logger.debug("Path {} not yet cached (key: {}), signaling as missing".format(path, key)) return abort(404)